Previous: Calls and returns, Up: Control Structures


5.8.6 Exception Handling

If a word detects an error condition that it cannot handle, it can throw an exception. In the simplest case, this will terminate your program, and report an appropriate error.

throw       y1 .. ym nerror – y1 .. ym / z1 .. zn error         exception       “throw”

If nerror is 0, drop it and continue. Otherwise, transfer control to the next dynamically enclosing exception handler, reset the stacks accordingly, and push nerror.

Throw consumes a cell-sized error number on the stack. There are some predefined error numbers in ANS Forth (see errors.fs). In Gforth (and most other systems) you can use the iors produced by various words as error numbers (e.g., a typical use of allocate is allocate throw). Gforth also provides the word exception to define your own error numbers (with decent error reporting); an ANS Forth version of this word (but without the error messages) is available in compat/except.fs. And finally, you can use your own error numbers (anything outside the range -4095..0), but won't get nice error messages, only numbers. For example, try:

     -10 throw                    \ ANS defined
     -267 throw                   \ system defined
     s" my error" exception throw \ user defined
     7 throw                      \ arbitrary number

exception       addr u – n         gforth       “exception”

n is a previously unused throw value in the range (-4095...-256). Consecutive calls to exception return consecutive decreasing numbers. Gforth uses the string addr u as an error message.

A common idiom to THROW a specific error if a flag is true is this:

     ( flag ) 0<> errno and throw

Your program can provide exception handlers to catch exceptions. An exception handler can be used to correct the problem, or to clean up some data structures and just throw the exception to the next exception handler. Note that throw jumps to the dynamically innermost exception handler. The system's exception handler is outermost, and just prints an error and restarts command-line interpretation (or, in batch mode (i.e., while processing the shell command line), leaves Gforth).

The ANS Forth way to catch exceptions is catch:

catch       ... xt – ... n         exception       “catch”

nothrow              gforth       “nothrow”

Use this (or the standard sequence ['] false catch drop) after a catch or endtry that does not rethrow; this ensures that the next throw will record a backtrace.

The most common use of exception handlers is to clean up the state when an error happens. E.g.,

     base  >r hex \ actually the hex should be inside foo, or we h
     ['] foo catch ( nerror|0 )
     r> base !
     ( nerror|0 ) throw \ pass it on

A use of catch for handling the error myerror might look like this:

     ['] foo catch
     CASE
       myerror OF ... ( do something about it ) nothrow ENDOF
       dup throw \ default: pass other errors on, do nothing on non-errors
     ENDCASE

Having to wrap the code into a separate word is often cumbersome, therefore Gforth provides an alternative syntax:

     TRY
       code1
       IFERROR
         code2
       THEN
       code3
     ENDTRY

This performs code1. If code1 completes normally, execution continues with code3. If code1 or there is an exception before endtry, the stacks are reset to the state during try, the throw value is pushed on the data stack, and execution constinues at code2, and finally falls through the code3.

try       compilation  – orig ; run-time  – R:sys1         gforth       “try”

Start an exception-catching region.

endtry       compilation  – ; run-time  R:sys1 –         gforth       “endtry”

End an exception-catching region.

iferror       compilation  orig1 – orig2 ; run-time  –         gforth       “iferror”

Starts the exception handling code (executed if there is an exception between try and endtry). This part has to be finished with then.

If you don't need code2, you can write restore instead of iferror then:

     TRY
       code1
     RESTORE
       code3
     ENDTRY

The cleanup example from above in this syntax:

     base @ { oldbase }
     TRY
       hex foo \ now the hex is placed correctly
       0       \ value for throw
     RESTORE
       oldbase base !
     ENDTRY
     throw

An additional advantage of this variant is that an exception between restore and endtry (e.g., from the user pressing Ctrl-C) restarts the execution of the code after restore, so the base will be restored under all circumstances.

However, you have to ensure that this code does not cause an exception itself, otherwise the iferror/restore code will loop. Moreover, you should also make sure that the stack contents needed by the iferror/restore code exist everywhere between try and endtry; in our example this is achived by putting the data in a local before the try (you cannot use the return stack because the exception frame (sys1) is in the way there).

This kind of usage corresponds to Lisp's unwind-protect.

If you do not want this exception-restarting behaviour, you achieve this as follows:

     TRY
       code1
     ENDTRY-IFERROR
       code2
     THEN

If there is an exception in code1, then code2 is executed, otherwise execution continues behind the then (or in a possible else branch). This corresponds to the construct

     TRY
       code1
     RECOVER
       code2
     ENDTRY

in Gforth before version 0.7. So you can directly replace recover-using code; however, we recommend that you check if it would not be better to use one of the other try variants while you are at it.

To ease the transition, Gforth provides two compatibility files: endtry-iferror.fs provides the try ... endtry-iferror ... then syntax (but not iferror or restore) for old systems; recover-endtry.fs provides the try ... recover ... endtry syntax on new systems, so you can use that file as a stopgap to run old programs. Both files work on any system (they just do nothing if the system already has the syntax it implements), so you can unconditionally require one of these files, even if you use a mix old and new systems.

restore       compilation  orig1 – ; run-time  –         gforth       “restore”

Starts restoring code, that is executed if there is an exception, and if there is no exception.

endtry-iferror       compilation  orig1 – orig2 ; run-time  R:sys1 –         gforth       “endtry-iferror”

End an exception-catching region while starting exception-handling code outside that region (executed if there is an exception between try and endtry-iferror). This part has to be finished with then (or else...then).

Here's the error handling example:

     TRY
       foo
     ENDTRY-IFERROR
       CASE
         myerror OF ... ( do something about it ) nothrow ENDOF
         throw \ pass other errors on
       ENDCASE
     THEN

Programming style note: As usual, you should ensure that the stack depth is statically known at the end: either after the throw for passing on errors, or after the ENDTRY (or, if you use catch, after the end of the selection construct for handling the error).

There are two alternatives to throw: Abort" is conditional and you can provide an error message. Abort just produces an “Aborted” error.

The problem with these words is that exception handlers cannot differentiate between different abort"s; they just look like -2 throw to them (the error message cannot be accessed by standard programs). Similar abort looks like -1 throw to exception handlers.

ABORT"       compilation 'ccc"' – ; run-time f –         core,exception-ext       “abort-quote”

If any bit of f is non-zero, perform the function of -2 throw, displaying the string ccc if there is no exception frame on the exception stack.

abort       ?? – ??         core,exception-ext       “abort”

-1 throw.