Safe Scripting: Check Before You Do Anything!

By Daniel Wood, 15 December 2010

When writing a script to carry out a particular task, ask yourself

"What conditions need to be true for this script to run properly?"

Sometimes, a script will do something which can potentially fail, and cause a cascade effect or errors, IF the initial conditions were incorrect. It is always a good idea to check for suitable conditions before doing anything in the script.

Updating a record

Consider a script that carries out a number of various operations on the current record. What do we need for the script to run properly? The answer is - a record. If your found set consists of 0 records, then why bother running the script? This is a potential scenario, for example, if you do a search which yields no results, your found set will be empty. If a user then tries to run the script, chances are pretty much every script step is going to fail.

This can be avoided by inserting the following few lines of code at the beginning of your script:

 

If [ Get ( FoundCount ) = 0 ]
Exit Script []
End If

This simple If statement will check if you have at least one record in your found set, otherwise it will exit immediately.

In fact, it is good practice to follow this guideline when writing all of your scripts. You should first carry out any checks or tests to ensure conditions are right for the script to begin executing. Exit the script immediately if any of the conditions are not right. After your error checking, just begin the script as usual - if execution reaches the point beyond your tests, you know that the tests all passed.

Structuring your error checking

Consider the following example - you have a script that is programmed to duplicate the current record, and do some various actions on the new record such as setting fields, BUT ONLY if the value of the 'status' field is "Completed".

Quite often, this script is written in the following style:

 

If [ Status ≠ "Completed" ]
Show Custom Dialog [ "Error" ; "Cannot duplicate if record is completed" ]
Else
Duplicate Record
Set Field [ a ; "a" ]
Set Field [ b ; "b" ]
etc.....
Show Custom Dialog [ "Done" ; "Record Duplicated" ]
End If

While there is nothing particularly wrong with this style, it can make for a script that is difficult to read. Now, consider the alternate style, of error checking, followed by carrying out the action:

 

/* Check for suitable conditions */
If [ Status ≠ "Completed" ]
Show Custom Dialog [ "Error" ; "Cannot duplicate if record is completed" ]
Exit Script []
End If

 

/* If script execution gets here, all tests successful */
Duplicate Record
Set Field [ a ; "a" ]
Set Field [ b ; "b" ]
etc.....
Show Custom Dialog [ "Done" ; "Record Duplicated" ]

The main difference is readability - all of your error tests are at the start of the script, and can quite easily be separated for ease of reading. You can group many checks by using "Else If" , or you can keep them individual using separate If/End If blocks.

The other difference is indenting of script steps - the majority of script steps are in the duplication area of the script, it is nicer to have the majority of the script not indented, rather than nested inside an If statement.

Checks within your script execution

Checking for suitable conditions may not necessarily only occur at the beginning of your script, sometimes they are required mid-way through your scripts execution.

Taking the first example one step further, lets assume this time that the record we are duplicating has potentially many child records that are displayed in a portal on the layout. When we duplicate the parent record, we also need to create duplicates of all of the children records also.

Our first check still stands - we can only duplicate when the record status is Completed. Even if the record has no child records, we still need to duplicate the parent record. So what happens when it comes time to duplicate the children?

 

/* Check for suitable conditions */
If [ Status ≠ "Completed" ]
Show Custom Dialog [ "Error" ; "Cannot duplicate if record is completed" ]
Exit Script []
End If

 

/* If script execution gets here, all tests successful */
Duplicate Record
Set Field [ a ; "a" ]
Set Field [ b ; "b" ]
Set Variable [ $ParentID ; Parent::ID ]

 

/* Now, duplicate the children records */
Go to Related Record [ show only related records; from table "Child" ; using layout (Child) ]
Loop
Duplicate Record/Request
Set Field [ Child::Parent ID ; $ParentID ]
Omit Record
Go to Record/Request/Page [ First ]
Omit Record
Exit Loop If [ Get ( FoundCount ) = 0 ]
End Loop
Go to Layout [ Original Layout ]

 

Show Custom Dialog [ "Done" ; "Record Duplicated" ]

Here, we test for the parent records condition, then we duplicate the parent record. Next, the script uses the go to related record step to make the found set be that of the children records, on the children layout, it then loops through the records, duplicating them and linking them to the new parent record, before returning to the original layout.

NEVER DO THIS !!!!

The script above fails to test whether there are any children records before it executes the duplication part! If there are no children records, then the Go to Related Records script step will NOT switch layout.

As a result, after the go to related record step, the layout & found set will remain exactly what it is on the Parent layout - the script continues and starts to duplicate parent records - duplicating every single parent record in the found set ! - This is a classic example of failing to check for error, and a script cascading out of control.

It is crucial that you script defensively, checking for any potential issue before it happens!

Now, consider the same script, with a small modification before the Go to Related Record code:

 

/* Now, duplicate the children records */
If [ not isEmpty ( Child::Child ID ) ]
Go to Related Record [ show only related records; from table "Child" ; using layout (Child) ]
Loop
Duplicate Record/Request
Set Field [ Child::Parent ID ; $ParentID ]
Omit Record
Go to Record/Request/Page [ First ]
Omit Record
Exit Loop If [ Get ( FoundCount ) = 0 ]
End Loop
Go to Layout [ Original Layout ]
End If

 

Show Custom Dialog [ "Done" ; "Record Duplicated" ]

Here, we are making use of the fact that a relationship exists between the parent & child records. We are doing a test to ensure that there exists at least one child record through the relationship. This is done by using the isEmpty function, checking the primary key field on the children table. A primary key field is guaranteed to have a value in it for every record. So by checking for the existence of a value through the relationship - we are actually checking for the existence of a record. All we care about is if there is at least one record, so this test is sufficient.

If there is a record through the relationship, then the block of code for duplicating child records can execute, otherwise the If statement contents is not executed at all.

Earlier, I noted that you should try to avoid indenting large blocks of code if at all possible - unfortunately this is one time where it is not possible when doing a check like this (FileMaker lacks the ability to "go to" a specific part of a script, thus bypassing a block of code).

One alternative you could employ however, is to create a new script which is responsible for duplicating child records, and your code can then be tidied up as such:

 

/* Now, duplicate the children records */
If [ not isEmpty ( Child::Child ID ) ]
Perform Script [ "Duplicate Child Records" ]
End If

 

Show Custom Dialog [ "Done" ; "Record Duplicated" ]

Final Thoughts:

The more defensive you are with your scripting, the less problems you will have later on. It is a good idea to ensure at the beginning of your script that you test for the conditions (or lack of) that need to be present for the script to execute correctly, such as:

  • Whether you have a record
  • Whether one or more field contents is what you need them to be
  • Whether you have any related records if required
  • Anything else you can think of that may need to be true in order for the script to run properly

Create yourself a section at the beginning of your script to place these checks, you can use comment script steps to detail each test, and to keep the tests separate from the actual execution of the rest of the script.

Checking for tests at the beginning of the script not only saves the script executing incorrectly later on, it also saves time by not running the script if conditions are not right. While some scripts might run without trouble even if conditions are not right, it will save time to not run them at all rather than run them and have them do nothing of any use !

Error and condition checking is not restricted to just the beginning of your scripts, it may be necessary to test for conditions mid-way through a scripts execution. If possible, try to test for the condition before running the script action - this will save you post-action error checking. Better to not do the action at all, rather than do it, and then have to check if something went wrong, and restore anything that may have happened.

Something to say? Post a comment...

Comments

  • Daniel Wood 04/03/2013 8:03pm (9 years ago)

    Hi Glenn,
    To understand why Exit Script is used instead of Halt Script, I'll briefly explain what each step does. The exit script step will when executed stop the running of the currently running script only. If another script that we will call 'A' called script 'B', and then 'B' executed an exit script step, then script A would continue to execute after its call to script B. However if 'Halt Script' is used in script B, then when it executes that step, both scripts B and A will cease to continue running. To put it another way - halt script will stop not only the current script but any other calling scripts / subscripts etc. The exit script step will only stop execution of the currently running script.

    It is for this reason I favor using exit script in almost all instances because more often than not in scripting if you want to prematurely stop execution of a script, you are only wanting to stop that one script, and not any others that may have potentially called the exited script. Only use the halt when you want all scripts to stop executing (which in practice I have found to be a very rare occurrence).

    Consider another scenario where you write a generic script to do something. Initially it is called direct and so halt/exit could be used with no difference. But later on someone finds the script and decides to use it as a sub-script in one of the other scripts (given it is a routine generic script it is possible this would happen). However if you used a halt script step, it's going to stop execution of the main script which is normally not intended behavior.

    hope that clears it up.

  • Glenn 03/03/2013 4:13pm (9 years ago)

    I'm brand new to FileMaker, and I'm curious why you chose the "Exit Script" step in lieu of the "Halt Script" step. Thanks, Glenn.

  • kent 26/04/2011 7:42pm (11 years ago)

    Daniel,
    I know I've been caught out a couple of times using this check on it's own and ended up with results back when not expected - but I confess they were most likely linked to checking a calculated field. I skipped FM7 & 8, so wasn't aware of that bug.

    These days, I generally use both the pre-check and post check. While I agree the pre-check is just good practise, not doing the post-check is making an assumption things will always work.

    I once spent hours trying to reproduce, then debug an apparently random issue. The cause was a linked to a field validation failing in rare situations, so when I tried to exit / leave the record (having passed the pre-check), it was failing.

    Kent.

  • Daniel Wood 26/04/2011 7:22pm (11 years ago)

    Hi Kent, thanks for the comment.

    I've just run a couple of tests but am unable to reproduce your results as mentioned. the isEmpty function will return false if the related field is deleted, the relationship is broken, or if the related table occurrence itself is deleted, none of these scenarios yield a true result. I am not sure what you are referring to by wrong start point, and deleting a layout should have no effect given it's the table occurrence that is the key thing here, layouts are irrelevant.

    Can you outline a scenario where ? would be returned through the relationship? The only one I could possibly envision is if you are testing through the relationship a field which is a calculation field which returns ? as a result itself, perhaps via a recursive custom function that runs out of memory, or is inherently broken.

    In that situation then yes I guess it would fail, however I think those scenarios are pretty rare. Combined with the rule that I always test the primary key field through the relationship which itself is a serial and not a calc, so guaranteed to have a value, I think its a safe bet.

    One of the other reasons why we adopted this method at DF is that a while back around FileMaker 8, there was a bug with the go to related record script step that would yield no error, even though there was error. It was an infrequent bug, but happened enough to cause us great pains until we figured out what was going on. What actually was happening was that the GTRR step would fail if executed at approximately the same time that a layouts order in the layout list changed, a layout was created or deleted etc. The GTRR ended up using the layout ID of a now different layout, but it still apparently executed without error, but unfortunately the user was now on the wrong layout, and so error checking scripts that passed the tests, continued to execute on the wrong layout, with disastrous effects as you could imagine!

    We made a change to our error checking that in addition to checking get(lasterror) it also checked the current layout name with that we expected the user to now be on if the GTRR passed... eventually all our GTRR's were cluttered with so much error checking it was getting ridiculous. When FM9 fixed this issue, we scrapped all of that error checking, and figured there was little use to still continue with post-GTRR error checks, which prompted to move to pre-checking.

    You raise a good point however in the whole pre/post checking discussion and I appreciate that. I'm sure everyone has their own viewpoint on when is the best time to check for errors, hopefully this can encourage more debate on the subject :)

    Thanks again,
    Daniel

  • Kent 26/04/2011 7:01pm (11 years ago)

    Hi Daniel,

    While the check for if[ not isEmpty( Child::ChildID ) ] is a good tip, FileMaker can potentially return ? if something else is wrong, which then causes the condition to be true. There are also other reasons the Go to Related Record may fail. (eg. Wrong Start point, File Missing, Structure change, layout deleted, etc)

    My preference (maybe in addition to the above test) is instead to check for an error immediately after.

    Go To Related Record[ Child::ChildID ; Show only Related Records ]
    if[ Get( LastError ) = 0 ]
    Loop
    Duplicate Records
    End Loop
    end if

  • Daniel 04/01/2011 8:01am (11 years ago)

    Hello Deborah. You are absolutely correct, the operator should have been not equal to instead of equals in those two cases, nicely spotted. I have fixed these both up now, thanks again!

  • Deborah 04/01/2011 6:01am (11 years ago)

    Being newish to FMP scripting - only been at it for 15 years and still feel green - I am a bit confused about the solution to the section "structuring your Error Checking" It looks to me as if the dialog will run and not duplicate a record if the status IS set to "complete". Is there something I am missing on this or should the operator be "not equal to".

RSS feed for comments on this page | RSS feed for all comments