General overview about Error Handling in FileMaker

Error handling is a big deal; and this website could stand to have at least one, if not several, pages dealing with it. The following collected ideas relate to error behavior in scripting, but it may become appropriate to split recommendations into separate pages for Standards, Best Practices, and Accepted Techniques based on the discussion of these issues. Ideas may be added or removed in the process. Please make suggestions.

"Error handling" is an umbrella term for how an application programmatically responds to errors. It's also too vague for a comprehensive discussion of error handling behaviors. A script can react to an error in any of several ways, and may respond to any one error with more than one of these behaviors:

  • Detect the error, which can include identifying what error occurred.
  • Log the error — Record that an error occurred, what error occurred, and the conditions of the error (timestamp, script, layout, table, etc.). Error logging is useful for debugging, but does not necessarily play any role in regulating normal application workflow.
  • Report the error — Notify the calling script or the end user that an error has occurred, and what error occurred. This is not the same action as logging an error, in that reporting an error normally does affect application workflow.
  • Resolve the error by performing some alternate steps that still satisfy the external expectations of normal execution of that script.
  • Abort on error — After detecting an error, cease executing the normally expected behavior of the script. A script can perform any reasonable clean-up before aborting and returning control to the calling script or the end user; for example, a script might close any opened off-screen utility windows before exiting.

It is also useful to distinguish between an error and a fault in execution of a script. A fault is something that went wrong; an error is what a script detects when a fault happens. For example:

  • If your thumb drive explodes while trying to open a file, you might get FileMaker's error 100 (File is missing). The exploding thumb drive is the fault; the missing file is the error.
  • If you loop through a set of records with the Go to Record/Request/Page [Next; Exit after last] script step, you'll get an error 101 (Record is missing) after the step tries to go to the record after the last record, which of course doesn't exist. "Record is missing" is the error. There is no fault; the loop was supposed to do that.

Some possible guidelines to consider when scripting error behavior:

Resolve an error, or report it, but not both. A script that encountered and resolved an error did manage to complete its expected task, so reporting the error is likely to lead to problems when a user or calling script takes action assuming the task was not completed. So either resolve, or abort and report. This corresponds to the strong exception guarantee.

Use an error dictionary custom function. Some functions already exist that retrieve human-readable descriptions of FileMaker-generated error codes. (For example, ErrorString.) If you use one of these functions, remember to augment it with any additional custom error codes you use in your solutions.

When reasonable, respect the error capture state that was active when a script was initially called, i.e., don't present errors to users if a parent script is capturing errors, and therefore assuming responsibility for reporting errors to users. A parent script has broader context, and is better positioned to provide a helpful response to an unresolved error. This idea might be extended by differentiating script roles along similar lines to the model-view-controller model.

# Beginning of script
Set Variable [$errorCaptureOn; Value:Get ( ErrorCaptureState )]
Set Error Capture [On]
...
If [$error and not $errorCaptureOn]
	# Report error to user
	Show Custom Dialog [Get ( ScriptName ); "Encountered error: " & $error]
End If
...
# End of script
If [not $errorCaptureOn]
	Set Error Capture [Off]
End If

When reporting an error to a user, include steps to take to address the problem. For example, a "Free Lunch" script may ask the user to pre-pay their FileMaker developer $1 million before delivery of free lunches can begin.

  • No labels

11 Comments

  1. Nice start Jeremy. If you've looked at my custom functions in the repository then you may be able to decipher how I'm handling errors - or not (it's a bit of a mishmash of evolution).

    I use more of the try/catch that java uses and throw exceptions. However, I centralize my error handling by using dedicated scripts based on error type - rather than dealing with them inline at the time they occur. I'm handing them off to an error handler script.

    Since there are classes of errors, I think we should define those. Here are the errors I've come across.

    FileMaker Errors
    Solution Errors (your own)
    Plugin Errors (plugins)
    Custom Function Errors (in particular Custom List - if you've used it)
    Developer Errors (for stupid programming)
    

    As you can see per my CF named Error() this one function pre-sets an assumed error type of FileMaker. This can be overridden with a variable of the same name which is defined after a call to Error().

    The Error() CF in turn uses ErrorData(), which is where all data relevant to when the error happened is captured. It also uses the ErrorString() to get the string of the error.

    My goal for error handling was to centralize. So here is an example of my handling.

    #
    #==============================================
    # Purpose:      Check for the download folder
    # Parameters:   $path
    #               ---------------------------
    #               $path = os path of folder to create
    # Called From:  (script) any
    # Author:       Matt Petrowsky
    # Notes:        none
    # Revision:
    #==============================================
    #
    #----------------------------------
    If [ #AssignScriptParameters ]
        If [ not TSFileExists( $path ) ]
            Set Variable [ $created; Value:TSFileMkDir( $path ) ]
            If [ not $created and $$APP.OS.FILEVAULT ]
                Perform AppleScript [ Calculated AppleScript: List(
                    "try";
                    "do shell script \"mkdir -p '" & $path & "'\"";
                    "end try";
                    ) ]
        End If
        Perform Script [ "Plugin Handle Error"; Parameter: List (
            Error;
            # ( "$errorType" ; "plugin" );
            # ( "$pluginError" ; plugin.error );
            # ( "$errorHalt" ; False );
            # ( "$errorDialog" ; False );
            # ( "$errorCapture" ; True );
            ) ]
        End If
    Else
        Perform Script [ "Developer Error"; Parameter: DeveloperError ]
    End If
    Exit Script [ Result: TSFileExists( $path ) ]
    

    You can see that I broke things down into error "classes" or types (as stated above). The script above calls the Plugin Handle Error wherein I have an error handler script for each type of error. This isolates the types of errors out, making them easy to distinguish from each other.

    Note the call to Error() first which pre-sets and then is overridden by setting a subsequent $errorType.

    I took this approach because the most basic solution may only have FileMaker errors to worry about and using a system of overrides provides more flexibility.

    And here is an example of my standard error handler script.

    #
    #==============================================
    # Purpose:          Error handler
    # Parameters:       error, type, script, data
    #                   ---------------------------
    #                   $error = (num) error number FMP or override
    #                   $type = (enum) CL, FMP - where FMP = FileMaker, CL = CustomList
    #                   $script = (string) name of script which had error
    #                   $data = (string) any inbound error text, like from CustomList function
    #                   ---------------------------
    # Called From:      (script) any
    # Author:           Matt Petrowsky
    # Notes:            none
    #==============================================
    #
    # Possible inbound variables
    # $error = Boolean if there was an error
    # $errorNum = Default is FMP error number but can be overriden
    # $errorType = "fmp", "cl", "app", "plugin" (cl = CustomList error)
    # $errorString = Typically FileMaker's error string, can be overriden
    # $errorScript = Scriptname where error occurred
    # $errorData = Environment data, plus error info
    # $errorLog = List of multiple errors from one single script
    # $errorHalt = Boolean of whether or not to halt out
    # $errorDialog = Boolean of whether to show dialog
    # $errorCapture = Boolean of whether to capture or not
    #----------------------------------
    If [ #AssignScriptParameters ]
        Set Variable [ $customListError; Value:$errorType = "CL" and Left ( $errorData; 6 ) = "[Error" ]
        Set Variable [ $filemakerError; Value:$errorType = "fmp" and $errorNum ≠ 0 ]
        Set Variable [ $appError; Value:$errorType = "app" and $errorNum = -1 ]
        #----------------------------------
        # Only deal with the errors we want to
        #----------------------------------
        If [ $filemakerError or $customListError or $appError ]
            #----------------------------------
            # Developer handling
            #----------------------------------
            If [ Developer ]
                Perform Script [ "Show Dialog ( method, title, content )"; Parameter: # ( "$method" ; "filemakercancel" ) &
                    #( "$title" ; "Developer software error" ) &
                    #( "$content" ; "The script \"" & $errorScript & "\" caused error " & $errorNum & ¶ & $errorString & ¶ & $errorData ) ]
                If [ Get ( ScriptResult ) = "cancel" ]
                    Halt Script
                Else
                    Exit Script [ ]
                End If
            End If
            #----------------------------------
            # Default handling
            #----------------------------------
            Perform Script [ "Error Cleanup" ]
            If [ $errorDialog or IsEmpty ( $errorDialog ) // default is to show dialog ]
                Perform Script [ "Show Dialog ( method, title, content )"; Parameter: # ( "method" ; "filemaker" ) &
                    #( "parts" ; $$UI.PROMPT.ERROR.GENERAL ) ]
            End If
            If [ $errorCapture or IsEmpty ( $errorCapture ) // default is to capture ]
                Perform Script [ "Capture Error ( errorData )"; Parameter: # ( "$errorData" ; $errorData ) ]
            End If
            # Send error via web site
            If [ $$LOGGEDIN ]
            Set Variable [ $subject; Value:"Error: " & $errorNum ]
            Set Variable [ $body; Value:List(
                "SCRIPT: " & $errorScript;
                debug.user;
                "---------------------------------------";
                Debug;
                ) ]
            Perform Script [ "Mail Error ( subject ; body )"; Parameter: List(
                # ( "$subject" ; $subject );
                # ( "$body" ; $body );
                ) ]
            End If
            If [ $errorHalt or IsEmpty ( $errorHalt ) // default is to halt on error ]
                Halt Script
            End If
            #----------------------------------
        End If
    Else
        Perform Script [ "Developer Error [ script ]"; Parameter: #old ( "script" ; Get ( ScriptName ) ) ]
    End If
    #----------------------------------
    #
    # RESULT
    Exit Script [ ]
    

    To top this all off. I would suggest we define specifics such as a table named Errorlog. My Errorlog table simply captures all error information by creating a new record. Each field within the Errorlog uses an auto-enter calculation which references any of the possible $errorVariables. It does this only if $errorCapture is True.

    This makes it very straight forward to capture and deal with errors.

    I certainly think my own system can be refined and improved.

    Let's start to define some specifics.

    1. For the sake of simplicity. I would suggest we take the approach of managing an errorPackage (which we define in the spec) which includes all error related information. You can see in my script above that I have over 10 $errorSomething variables and $errorData is packed with all kinds of extra stuff that is useful for debugging/troubleshooting - and can be logged.

      It's also important to note that it's not a good idea to simply start using any range of numbers for solution errors. For example. Just because you start your own errors at 5000 and above does not preclude FileMaker from using that range themselves. Unlike 192.168.0.0, FileMaker has not declared a "safe" range for errors.

      This is why I propose we use an $errorType which allows developers to use any numerical range desired.

      1. An Error pseudo-struct? I like it. Since $errorData is packed with debugging info, why not suggest that this error structure simply include the same data as the Debug function (whatever that may be for a particular solution's purposes), in addition to the error specific information.

        My first instinct is to go on to suggest an error interface for several custom functions along the lines of UUIDGetTimestamp and UUIDGetNICAddress (ErrorCode ( $error ), ErrorType ( $error ), ErrorHostTimestamp ( $error ), etc.), but I'm more ambivalent on this one. That would be a lot of parameters to write functions for, but that's probably just the same resistance others are feeling against the Layout[LayoutName] functions idea. On the other hand, continuing to structure errors the way you've got them now follows the same format as the implementation we've got for name-value script parameters, and it would be so easy to just write #Get ( $error ; "errorCode" ) knowing that. (It would be even easier if the contents of $errorData were appended to the rest of the information rather than having a nested structure.) A compromise might be ErrorGet ( "errorCode" ), so that at least the interface is independent of the implementation.

        I like an $errorType parameter, but I think that should be included as another parameter to the ErrorString function. (While I'm thinking about it, the name "ErrorDescription" seems a bit more ... descriptive of what that function returns.) Then non-FileMaker errors have a centralized interpretation for any given solution, and the $errorString parameter can be excised from the error structure.

    2. It looks like your error handling example is following the arrow anti-pattern. (Oh the horror!) The second error handling example on the proposal avoids that with guard clauses.

      I think we have two different ideas of what we mean when we talk about throwing an exception. It seems to me that you mean it in the sense of how a script deals with errors internally: drop the error in some other script's lap. I mean it in the sense of how a script behaves with respect to another script that called it: if I can't handle the error, pass the buck up the chain of command to the calling script. Both approaches have their merits. Perhaps these should be separate Accepted Techniques pages?

      1. Ouch! The accusations! That arrow hurts. (not really - because I know I'm only two levels in for the default handling. And it's only when I get to level 3 that I start to evaluate if I should refactor. (wink)

        As stated, it's an evolutionary mishmash and needs to be cleaned up.

        I think it's admirable to take the "highbrow" approach and to educate on error vs. fault vs. exception vs. whatever, but for most of the developer's following this site I would assume they know "something went wrong". So the question is "What do I do when that happens?"

        I say we address both sides, returning exceptions and handling the faults. That's what most people are looking for - even if they don't know the difference. (wink) Defining this outside a CS context is what will make it the most approachable.

        1. I agree completely that making this as approachable as possible is worthwhile. There is plenty of revision to be done. One challenge we run into with that is that there's more than one good answer to "What do I do when that happens?", and there's some 'splaining to do to describe the differences and relative merits of different approaches. I'm thinking of the C.S. terms as a shorthand to be expanded in successive edits.

          I also think those in the FileMaker community with a more intellectual approach will appreciate that this content is more broadly informed. We are not an ignorant island re-creating the wheel. Those without a C.S. background who are curious about such things may get some value from a FileMaker-specific introduction to more abstract topics.

    3. I like the idea of expanding more on Error logging. Perhaps:

      • A Standard, reserving the Errorlog table name, and
      • A Best Practice, specifying the API for logging an error.

      It will also be worth discussing different patterns for when to log errors. For example:

      • Logging immediately after detecting an error, logging at some other point that's still in the same script, or waiting until "the case is closed" to log an error.
      • If an error is resolved, is it necessarily worth logging? (Yes. Duh! It's still an interesting question.)
  2. Wow...where have I been? All this great discussion on error handling and not a peep from myself. Oops...reality bites sometimes. :)

    Anyway, based on the work already done I combed through my day job solution to see if any patterns emerged on how others treat errors in a multi-developer environment. Here's a list of observations to discuss (or not):

    • The "Halt Script" step is still used too often, albeit less. Is there a reason to exit a script via Halt instead of Exit when an error is encountered?
    • Lots of scripts do not return a value via the Exit Script step. Our internal standards require a script result due to legacy arrow anti-patterns and getting lost as developers change.
    • We have no central error handling "ScriptClass". It's to each their own. That alone bites me daily, but I digress.
    • Core business transactional scripts typically use either Guard Clauses or Try-Catch depending on who developed the first instance. I see benefits both ways and use one vs. another depending on what the context is. Any ideas on how to model one way or another for consistency? Not seeing anything immediate come to mind...
    • There's at least three different attempts at error classification in various modules. Only the one I've introduced reserves the first attribute in a Script Result for FMP errors (a method above I highly agree with if using List () returns, otherwise "error" named parameter is reserved for FMP, imho).
    • While we have an "uberArchive" log database, script execution performance mysql database, and a development project management system we have no dedicated Errorlog table solely for logging scripting errors. I absolutely agree it's an excellent proposed standard.
    • In order to differentiate when a script is intended to run headless vs. report errors to the end user they've defined two basic camps: User Interface ( "UI" ) scripts can kick open dialogs to report errors where Business Transaction ( "BT" ) scripts may not, instead being required to report errors upstream to the calling script.

    Enough on that for now...just adding some food for thought from a system that's seen all types and kinds of users and developers.

    PS: Thanks for the reminder on respecting error capture state. That's just good stuff to write down somewhere!

    1. Perren, in attempt to make things as simple for myself as possible, I use a centralized Error Handling script.

      This can then branch based on the error types. I've classified errors as being one of the four possible categories.

      • FileMaker (fmp) Error
      • Solution (app) Error
      • Plugin (plugin) Error
      • Custom Function (func) Error

      By using a system of reserved $errorVariables and overrides, I think my system is pretty straight forward. It takes more of the Try-Catch approach by simply making a call to a single script named Handle Error ( error ) right after any step which may generate an error. It uses the Error functions of

      • Error
      • ErrorData
      • ErrorString ( type ; num )
      • SetError ( type ; num ; dialog ; capture ; halt )

      I've put all this into the Standards.fp7 file as a start for refinement. It'd be great if you could walk through it and see what you think.

      Areas which need to be addressed are multiple plugin error handling and how Custom Functions might leverage this same system.

      Here's the link to get the file. Just right-click on Standards.fp7 and choose Save As...

      https://github.com/filemakerstandards/fmpstandards

  3. I think error handling would be more digestible if we break it up into a couple different pieces:

    • Error Trapping & Resolution Patterns (Techniques)
    • Error Information tracking (including the error "package" structure) (Technique? Best Practice?)
    • Error Logging (Best Practice)

    Any others?

    1. I think that's a great idea. Just add sub pages under this main page of Error Handling. I think most devs conceptually wrap the whole thing under the term "Error Handling", although the handling part is only one aspect - as you mention.

      We might be able to get away with just two categories, since most of what I have with the Error function stuff is error tracking - which leads into error logging.

      How about we start with

      • Error Trapping & Resolution
      • Error Logging

      Here's how I see my system fitting into Guard Clauses, where the Handle Error script simply centralizes and manages the dialog display and logging aspects. It would also act as a dispatch for the different error types if you wanted to branch for various plugin handling (which is what I've done in the past)

      Go to Layout [$layoutName]
      If [Get ( LastError ) = 105 //Layout is missing]
      	Handle Error ( # ( "$error" ; Error  & # ( "$errorHalt" ; False ) ) )
      	Exit Script [Result: # ( "$error" ; Error )]
      End If