Handling errors

Plug-in errors, Python exceptions, and how to handle them.

Plug-in errors in general#Back to top

Let's first see what we are dealing with.

How FileMaker handles plug-in errors#Back to top

FileMaker has a very simple notion of a function: a function is something that takes parameters and returns results. But in practice there's always a third piece of information that needs to be passed: whether the call proceeded as expected and if not, why?

Internally the information is (partially) there: deep at the C level a plug-in function actually returns one of FileMaker error codes and passes the actual result separately using one of parameters. This is limited, of course, because you cannot add your own error codes or pass additional information and also because FileMaker doesn't actually recognize different codes at this level; all it cares about is whether there was any error.

If there were no error (error code 0), FileMaker returns the result, possibly empty; if there was some error (other error code), FileMaker ignores the result and returns a question mark (?) instead.

PyFM actually tries to always return the correct FileMaker error code; if a Python function called, say, evaluate() or execute_sql() function and got an error, PyFM first wraps it into a Python exception so the calling code may intercept it. If this didn't happen, PyFM delivers the exception back to the top-level dispatcher and saves for PyLastError. If this is one of FileMaker errors, it converts it back into just a code and dutifully sends it back to FileMaker. (And if this is not a FileMaker error, PyFM returns the unknown error code (-1). It's a pity all this turned out to be useless.

How other plug-ins deal with errors#Back to top

A single question mark is not very useful, so over time plug-in writers devised their own methods to provide information about errors. There are two basic approaches:

  • Some plug-ins return the error code and provide a separate function to retrieve additional information. This is what I used in PyFM; the function is PyLastError.

  • Some plug-ins never return an error code but return a string that cannot possibly be a valid result. Usually this is a string with some unusual prefix; for example, Troi plug-ins use strings that start with $$-, followed by the error number.

    I think this one is less convenient, but if you prefer this approach, see below the PyAlt function which imitates it with PyFM.

What are PyFM errors#Back to top

Since the PyFM plug-in is powered by Python, all errors are Python exceptions. Python exceptions do not have codes; instead they have class. (They most certainly do :) For example, if you pass a text instead of a number, the usual response is to raise a TypeError. If the type is OK, but the value is not, it could be ValueError or maybe IndexError, or KeyError, depending on how it was used.

Class is mostly used in the code, to see what kind of exception it is. For example, some Python code may expect an error of a particular type to take a different course, e.g.:

try:
   do_something()
except (IOError):
   do_something_else()

This piece expects do_something() to maybe raise IOError and if this happens, do_something_else() instead. If do_something() raises a different error, such as MemoryError, the code will exit at this point.

In addition to class Python exceptions often have value. Not always; e.g. MemoryError usually has no value. The value is usually a string, but also not always; for example, the value of SyntaxError is a more complex structure that indicates the position of the error.

Class is mostly checked mechanically by the computer, but value is intended more for the human eye. Value usually contains much more detailed information about the error, showing not only a longer description of the class, but also relevant data: received argument type, value, name of the missing file, and so on.

There are lots of possible exceptions and they're decentralized: there are some standard Python exceptions, but each module is free to add its own.

How do you get error information in PyFM#Back to top

In PyFM you get error information using PyLastError. It returns the error, if any, occurred in the last PyFM call; you usually check it right after you called a PyFM function.

How PyFM errors look like#Back to top

The PyLastError function returns a single string that combines the class and the value, if any. If this is a standard Python error, the function uses only the class name; if not, it prepends the name of the module:

<class>
<class>: <value>
<module>.<class>
<module>.<class>: value

e.g.:

MemoryError
ImportError: No module named 'mymodule'
plugin.ReadOnlyError
filemaker.OutOfRangeError: Out of range.

Note the last one: this is a FileMaker error.

How FileMaker errors look like#Back to top

When you call FileMaker functions from Python, they can also return errors. Internally they return error codes, but PyFM immediately converts them into exceptions. For example, if you evaluate an expression that contains a name of a missing field, you'll get a MissingFieldError:

>>> from filemaker import evaluate
>>> t = evaluate('No Such Field')
Traceback (most recent call last):
    ...
MissingFieldError: The field is missing

Unfortunately, there's no way to get any extended information about which field is missing, so for FileMaker errors the value is always same and is simply a description of the corresponding error code.

List of all FileMaker exceptions#Back to top

To be done.

How to handle plug-in errors#Back to top

Let's first see which situations we need to handle. I'd say there are two cases:

  • There is an error with the plug-in itself.

  • There is an error with a function.

What to do if the plug-in is not there#Back to top

Many developers check for plug-ins once at startup and refuse to start if they're not there. I wouldn't recommend it. Often it is plainly rude, especially for plug-ins that are used infrequently for some specific tasks. It is also not robust enough, because technically the user can disable the plug-in after the application loads. Of course, this is a very remote possibility, but would you take this risk? I wouldn't.

The proper way to check if the plug-in is not there is to check this on every call. This is not difficult; all it takes is a single custom function.

How to detect an error in a function#Back to top

To detect an error in a function check the result. If it is a question mark, call PyLastError: if it returns something, then it is an error:

How to handle an error#Back to top

To handle an error means that you either take a different course or reclassify the error. The first is clear; the second means that you let the error to go up, but change it to a more specific one. For example, the underlying function may raise a generic error such as ‘Record is missing,’ but you reclassify it as ‘Cannot find a customer with code ABC123.’

This means you can only handle errors you expect. Since you know what you're going to get, it is not difficult to recognize the expected error when it happens. For example, I may recognize FileMaker's OutOfRangeError (error code 14) so:

If[ Left( PyLastError, Length( "filemaker.OutOfRangeError" ) ) =
        "filemaker.OutOfRangeError" ]
    ...
End If

If you're using FileMaker Advanced, write a custom function to simplify such checks; see StartsWith below.

How to handle errors you don't expect#Back to top

This is not a problem. You can always deal with any PyFM error: just display the error to the user and exit the script. This is also handling it, by the way; to handle an error is to stop it somewhere, and this is exactly what happens if you show it in a dialog.

Of course, showing a possibly technical message may not help the user much, but it's better than nothing or just an error code.

Which errors you can expect#Back to top

It's fairly difficult to list all errors one get from a Python function. As I said, errors are actually Python exceptions, and exceptions can be raised not by the function itself, but also by any function it calls, directly or indirectly.

For example, nearly every function can technically raise a MemoryError exception if it fails to allocate the required amount of memory and in most cases it will pass through to the very top.

I try to list possible errors in documentations for plug-in functions, but this list is not guaranteed to be complete. But if you're going to handle an error, you probably already know what kind of error it is going to be, so it's not a problem. All the rest you can handle in generic way.

Useful custom functions#Back to top

Here's a list of useful custom functions to handle errors. To copy them in your own FileMaker files you'll need FileMaker Advanced. Once they are there they will work in any other FileMaker edition.

Py#Back to top

This custom function receives a result of a PyFM call and passes it through as is, checking only whether there was an error. If there was, the function will save the error in the global variable $$error.

Py( pyfmCall )

Parameters
pyfmCalla call to a PyFM function

This must be a call to a PyFM function (except PyLastError) or a result of calling a PyFM function, that is either this:

Py( PyRun( ... ) )

or this:

Let( result = PyRun( ... );
  Py( result ) )
Result and side effects

The function returns the pyfmCall parameter exactly as it received it and also changes the contents of the $$error variable: if the call to PyFM went successfully, it clears the variable, else it sets it to the error message.

Code
/* Py( pyfmCall ) */
Let( $$error =
  Case pyfmCall = "?";
    Let( errorMessage = PyLastError;
      Case(
        errorMessage = "?";
          "This functionality requires the PyFM plug-in and/or a
          specific plug-in function, which doesn't seem to be
          available. Please make sure the plug-in is there and try
          again."
        not IsEmpty( errorMessage );
          errorMessage ) );
  pyfmCall )
Discussion

As you see the function has a message for a situation when a plug-in or a function is not there. You can then use it so:

Let[ $result = Py( PyRun( ... ) ) ]
If[ IsEmpty( $$error ) ]
    ...
End If

You can even combine the two steps together:

If [ Let( $result = Py( PyRun( ... ) ); IsEmpty( $$error ) ) ]
   ...
End If

PyEx#Back to top

PyEx() is a variation of Py() that takes an extra parameter: a safe value to use if there's an error:

PyEx( safeValue; pyfmCall )

Parameters
safeValueanything

The value to use if the call to PyFM fails.

pyfmCallcall to a PyFM function

Same as in Py(): a call to one of PyFM functions, except PyLastError.

Result and side effects

If the call to a PyFM function went successfully, return the result and clear the $$error variable, else return safeValue and set the $$error variable to the error message.

Code

The code uses the Py() function:

/* PyEx( safeValue; pyfmCall ) */

Let( result = Py( pyfmCall );
  If( IsEmpty( $$error );
    result;
  /* else */
    safeValue ) )

PyAlt#Back to top

If you prefer the combined approach, i.e. if you'd rather receive the error message instead of the question mark result, I'd suggest the following wrapper:

PyAlt( prefix; pyfmCall )

Parameters
prefix

Prefix to prepend to the error message. An empty prefix will return the message as is.

pyfmCall

Same as in Py and PyEx a call to one of PyFM functions, except PyLastError.

Result and side effects

If PyFM call went successfully, return the result of the call, else return the error message prepended with the specified prefix and save it in the $$error variable. The function has no side effects.

Code

The code uses the Py() function in the same manner as PyEx(), but flips the logic slightly:

/* PyAlt( pyfmCall ) */

Let( result = Py( pyfmCall );
  If( IsEmpty( $$error );
    result;
  /* else */
    prefix & $$error ) )

StartsWith#Back to top

Check if a string starts with a known prefix. Useful to test PyFM error types.

StartsWith( string; prefix )

Parameters
stringText

String to check.

prefixText

Prefix to check for.

Result and side effects

Return True if the string starts with the prefix, otherwise False. Neither string nor prefix are case-sensitive. The function has no side effects.

Code

The code is straightforward:

/* StartsWith( string; prefix ) */
Left( string; Length( prefix ) ) = prefix