FileMaker Fail Early

There is a principal in software design called "Fail Early”, "Fail Fast", or "Fail Early, Fail Fast”. For this post, I am going to use the term “FileMaker Fail Early” to share some thoughts about how this principal applies to FileMaker software development...

Let us look at the common use case where a user clicks a Button on a Layout that runs a Script. For the Script to succeed, there might be a number of conditions that must be true, for example, there might be:

  • Object Names that the Script references
  • Fields that must be empty (or not empty)
  • Related records that must be present (or not present)
  • Required Plugin(s) (with versions)
  • Feel free to add your favorites below
  • Users privileges. // thanks @harvest
  • Hidden errors that show up when you commit record // thanks @mrwatson-de
  • Share points are mounted // new, from yours truly

A “FileMaker Fail Early” strategy lets us keep our Script as simple as possible by failing (dare I say Halt Script'ing) before we change state...

  1. Layout
  2. Window(s)
  3. Found Set
  4. Etc.

…and then needing to "clean up".

We are polishing and updating our “FileMaker Fail Early” Scripts. Below is one:

  1. Which of the 3 lines do you prefer: 8, 9, or 10?
  2. What do you think of the Script name?
  3. Other thoughts?

Considering that we put this Script in almost every File, how would you comment?

  • Comment each section in the script in-line
  • Single line comment at the top
  • Let the Script name be the comment
  • Link to a web page that documents all the “FileMaker Fail Early” Scripts
  • Other?

This post is, in part, a followup on this presentation
DIGFM: Code Review, ♫ It's Getting Better All the Time ♬ (5/9/2024)
https://www.youtube.com/watch?v=oPZ_sJHVHVM&t=2288s

The goal of the presentation was to show some good code and some bad code...easy to do if you [don't] think about it :wink:

"Failure is an option” — unless you are watching a movie

Here is an example of how the above Script might be called:

Fail early is a great philosophy!

It's MUCH better to fail in the hands of the developers than the customers! Support time is gold!

You, of course, can say HALT SCRIPT! In some cases it is a much more desirable result than the consequences of NOT halting!

For example, we have a FAIL FAST & halt early script to create a record. It checks to see if it failed - which happens if somebody had locked the table by working live on the schema - and retries for a short time and then gives up and halts the script.

Although this is an unpleasant outcome for the user - and the data - it is by far a lesser evil than the new record step quietly failing and the rest of the script continuing to work on some random record, whatever was currently in focus before the script started!

3 Likes

if the script is going everywhere, or being used a lot, then that construction would be better placed into a custom function, no?

Calling LayoutObjectExists( objectName ) seems easier than writing all that code many times over.

2 Likes

Thanks for your thoughts...

A single Script per File that is called from many Scripts is a good architecture (IMO).

A script is needed to Show a Custom Dialog and sometimes to perform a subscript. A single Script does it all.

I would say that a Custom Function is not called for in this case because:

  • A single Script per File is already "Don't Repeat Yourself" (DRY)
  • Using a Custom Function would introduce a dependency and thereby reduce portability.
  • We would have 2 code objects (Script + CF) instead of one (Script) = more complex
  • There is not much complexity to encapsulate, especial after we remove at least 2 lines as noted above.

Here are some thoughts on Custom Functions from the same presentation:

DIGFM: Code Review, ♫ It's Getting Better All the Time ♬ (5/9/2024)
https://www.youtube.com/watch?v=oPZ_sJHVHVM&t=6247s

1 Like

Thank you all for that discussion,

this topic is of great interest. I never heard of "fail early" before, but that make a lot of sense !

Hi Tony,

while I hadn't put a name to it I try to follow similar patterns for scripting.

I try to cover as much as possible of the pre-checking routines before touching anything like changing the layout, entering fields etc. too.

First is always a check for the users privileges.

Second is a part of the scriptparameter that already covers prechecks if the button calling the scipt is pushed like looking for related records needed.

Third is looking for an error message coming from a previous run of the triggered script that got initiated by unsucessfully entering data that failed plausibility checks and the likes

In case or CRUD scripts I go for the deletion part next asking the user to again state his intention and than turn data ( and related data ) invalid

...so getting the low hanging fruit first and of course give as much meaningful response messages as possible to the user.

and by the way I would opt for line 8 because I like the longer approach that will help anybody taking my programming at some point in the future to better understanding and it gives me more opportunities maybe not in this case but in more complicated scenarios

best
Holger

Thank you for your thoughtful answers.

Good catch, we do that too.

We sometimes commit the record to bring forward any errors lurking in uncommitted changes from previous actions that are not related to the Script we are about to run.

Cool.

Thank you for voting and providing a reason for your vote!

Just about every script we create at D-Cogit goes through these steps:

  • Initialization;
  • Validation;
  • Script logic;
  • Cleanup;
  • Return.

The "fail early" principal would be handled in the validation step. That said, we don't use a set strategy during validation. Each script has its own needs, therefore validation is specific to each script. Of course we use core scripts and custom functions to handle repetitive routines.

We also do our best to use the MVC (model-view-controller) design pattern. We have scripts for layouts / menus (view), for business logic (controller) and for data (model). The "fail early" strategy is not always possible because some error-checking is limited to specific domains (model, view or controller).

Lastly, our script code is structured such that execution always starts at the start of the code and always ends at the end of the code. Exiting anywhere else is prohibited. Halting is prohibited because it can have unintended consequences in MVC.

Hope this helps!

3 Likes

Updated the original post to add 3 conditions to check early:

  • Users privileges. // thanks @harvest
  • Hidden errors that show up when you commit record // thanks @mrwatson-de
  • Share points are mounted // added by me...better late than never!

More fail fast tactics:

  • my fail first programming ethic is catch early!

So early, in fact, that I never allow some bugs to even get into the database, specifically:

  • we use Mac + MBS plugin and fmSyntaxColorizer to catch steps at the coding stage. Before the script is even run.

  • the most important feature is MBS variable checking which highlights all variables which are used but not defined

    • to make this work you MUST define all variables using Set Variable - NOT Let($var…;…)!
    • that means forgoing automatic parameter functions like #(…)
    • all script parameters are defined at the top of the script
    • all loop or list variables are initialised (to 1 or "" respectively)
    • this not only makes it possible to catch (virtually) every variable name typo, but also means your script parameters are always documented at the start of a script
  • fmSyntaxColorizer also highlights interactive steps (with dialog, pause or with halt), so that you notice more easily and quickly that you've forgotten to turn of an Enter Find Mode pause or similar.

Another technique I use to fail fast by catching quickly is to avoid long if blocks with an else block off the page.

If the error handling is a few lines to just log an error, show a dialog maybe with an exit or halt, I prefer to put the error handling first, and the longer ' normal' part of the process in the Else block.

Like this, when you see the else block, you can still see the If condition on the page and thus know instantly when the else block is performed/what it is for.

If you program the other way round - preferring to have the main flow first and the edge cases / error handling last (which does have its merits) this can - particularly if you have (lots of) nested ifs - lead to a string of else-error blocks and way disconnected from their branching logic.

Such code can quickly lead to errors like this:

If A ok
  If B ok
    # do stuff
    # do stuff
    # do stuff
    # do stuff
    # do stuff
    # do stuff
    # do stuff
    # do more stuff
    # do more stuff
    # do more stuff
    # do more stuff
    # do more stuff
    # do more stuff
    # do more stuff
    # do more stuff
    # do more stuff
    # do more stuff
    # do more stuff
    # do even more stuff
    # do more stuff
    # do more stuff
    # do more stuff
    # do even more stuff
    # do more stuff
    # do more stuff
    # do more stuff
    # do even more stuff
    # do more stuff
    # do more stuff
    # do more stuff
    # do even more stuff
  else
    Error A
  End if
else
  Error B
End if

The other disadvantage of this structure is that the errors are in reverse order , when programmed like this correctly - which is illogical, hard to check when scrolling up and down and checking indentation level - and a really unnecessary brain strain!

The other way round is MUCH easier to read, code and to see (I.e. catch/ avoid) errors AND can mostly be flattened into a single simple If Else If Switch construct:

If A NOT ok
    Error A
else  If B NOT ok
  Error B
else
    # Ready to go …
    # do stuff
    # do stuff
    # do stuff
    # do stuff
End If

If there is no clean up code, it is also possible to code this as guard clauses, which exit the script before you have started.

If A NOT ok
    Error A
    Exit Script 
else  If B NOT ok
  Error B
    Exit Script 
End If

# Ready to go …
# do stuff…

Or the even more compact version (my fav)

Set Variable [$errMsg ; List (
  Case ( not A ok ; "Error A" ) ;
  Case ( not B ok ; "Error B" ) ;
  …
  )) ]
If [ not IsEmpty ( $errMsg )
    Exit Script [ $errMsg ]
End if

# Ready to go …
# do stuff…

I also use fmLogAnalyser to catch pasting errors logged in the FileMaker import.log file before they go unnoticed!

Conclusion:

  • Fail fast by catching the errors at coding time!
  • Don't let the bugs in! // = Google my presentation on this topic (Rome? Dotfmp? Somewhere😂)
3 Likes

I have a two-line error check at various points that will exit right at fail point

	Loop

		…
		Set Variable [ $error ; Value: Case( Get( LastError ) > 0; JSONSetElement( ""; [ "code"; Get( LastError ); JSONNumber ]; [ "message"; "foo"; JSONString ] ) ) ] 
		Exit Loop If [ JSONGetElement ( $error ; "code" ) > 0 ] 

		…
		Set…
		Exit…


		Exit Loop If [ True ] 
	End Loop




# clean-up
If [ JSONGetElement ( $error ; "code" ) > 0 ] 
	
	Revert Record/Request [ With dialog: Off ]  //error
	Set Variable [ $result ; Value: JSONSetElement ( "" ; ["error" ; $error ; JSONObject] ; ["data" ; "{}" ; JSONObject] ) ] 


Else
	
	Commit Records/Requests [ With dialog: Off ] //success 
	Set Variable [ $result ; Value: JSONSetElement ( "" ; ["error.code" ; 0 ; JSONNumber] ; ["error.message" ; "success" ; JSONString] ; ["data" ; "{}" ; JSONObject] ) ] 
	 
End If


Exit Script [ Text Result: $result ] 

3 Likes

This is the way!

And if you encapsulate that error authoring/detecting logic, you can even do:

Loop
    …
    Exit Loop If [ Let ( $error = Error.Last ( "foo" ) ; Error.IsError ( $error ) ) ]

    // I actually go further and allow the CF to magically populate $error, and avoid that boilerplate, but magic is controversial.
    …
    Exit Loop If [ Error.CheckLast ( "foo" ) ] // sets $error and returns true if is error


    Exit Loop If [ True ]
End Loop

If [ Error.IsError ( $error )
    // handle error
End If

// Bubble it up!
Exit Script [ JSONSetElement ( "{}" ; "error" ; $error ; JSONObject ) ]
1 Like

Here is Script that we call as many times as needed to check that a field is NOT empty. For example, it takes 10 lines in the calling script to check 10 fields and get field specific feedback (keeps the calling script short and easy to read).

This one is part of a suite that can check for Empty, Not Empty, = 0, > 0, etc., etc.

I DRAFT'ed a vRev of the Sub-Script that I use to "Fail Early" when a field IsEmpty.

The goals for this vRev where:

  • Use FM 16 JSON Functions to simplify the code
  • Add an optional Go to Object {GTO} feature. We considered adding optional Go to Layout and/or Run Script and decided that would be too much and not needed most of the time.
  • Simplify and make as readable as possible the passing of the ( field_ref, {custom_message}, and {object_name} ).

Below is the DRAFT Sub-Script: "_If [field] IsEmpty, {message}, {GTO}, Halt_P"

  • Since this Script will be used in many system and many Files, we might move most of the comments to a more central location
  • We are using a Custom Function: @field_msg_obj ( field_ref ; message_custom ; optional_objectName ) // to simplify and make as readable as possible the passing of the parameter to Script:_If [field] IsEmpty, {message}, {GTO}, Halt_P

1/4: Script:_If [field] IsEmpty, {message}, {GTO}, Halt_P

2/4: Custom Function: @field_msg_obj ( field_ref ; message_custom ; optional_objectName )

3/4: Example calling Script

4/4: We are testing the use of the InFocus State to turn the background from white to light blue:
To blue or not to blue, that is the question. Thoughts?

Feedback welcome.

Feedback welcome.

# _If [field] IsEmpty, Go to Layout, Go to Object, then Halt_P in file Contact (fms dot twdesigns dot com)

// 2024-12-25 by twdesigns dot com - TW:
// Script is called with a Parameter built using a Custom Function: @FLOM ( GetFieldName ( Contact::Terms ) ; 168 ; "f.Terms--Contact" ; "Enter Terms" )
// Custom Function: @FLOM ( field_ref ; layout_ID ; object_name ; custom_message )

Set Variable [ $json ; Value: Get ( ScriptParameter ) ]
Set Variable [ $field.TO_FN ; Value: JSONGetElement ( $json ; "field_ref" ) ]
Set Variable [ $layout_ID ; Value: JSONGetElement ( $json ; "layout_ID" ) ]
Set Variable [ $object_name ; Value: JSONGetElement ( $json ; "object_name" ) ]
Set Variable [ $message.custom ; Value: JSONGetElement ( $json ; "custom_message" ) ]

// value.index.ci ( a_list ; a_value ) // FileMaker Custom Function: value.index.ci ( a_list ; a_value )
⌘/ Set Variable [ $layout_number ; Value: value.index.ci ( LayoutIDs ( "" ) ; $layout_id ) ]
Set Variable [ $layout_number ; Value: // replace for portability: value.index.ci ( a_list ; a_value ) Let ( [ ~list.padded = ¶ & LayoutIDs ( "" ) & ¶ ; ~value.padded = ¶ & $layout_id & ¶ ; ~position = Position ( ~list.padded ; ~value.padded ; 1 ; 1 ) ; ~list.padded.left = Left ( ~list.p… ]

Go to Layout [ $layout_number ; Animation: None ]

Go to Object [ Object Name: $object_name ]

Go to Object does cause a Drop-down List. Neither does the next 2 lines.
⌘/ Go to Next Field
⌘/ Go to Previous Field

If [ IsEmpty ( $message.custom ) ]
Set Variable [ $field.TO_FN_as_List ; Value: Substitute ( $field.TO_FN ; "::" ; "¶" ) ]
Set Variable [ $field.FN ; Value: GetValue ( $field.TO_FN_as_List ; 2 ) ]
Set Variable [ $message ; Value: "Field [" & $field.FN & "] can NOT be Empty." ]
Else
Set Variable [ $message ; Value: $message.custom ]

End If

// The internet says that Emoji support is based on the operating system
// :warning:AKA 'Warning' was approved as part of Unicode 4.0 in 2003,
// under the name "Warning Sign" and added to Emoji 1.0 in 2015.
Show Custom Dialog [ ":warning:" ; $message ]
⌘/ 1 = OK
Halt Script

Happy Holidays