5. Exceptions
Overview
Fantom uses exceptions as the standard mechanism for error handling. An exception is a normal object which subclasses from Err
. Exceptions are thrown or raised to indicate an error condition. When an exception is raised, normal program execution is disrupted and the call stack is unwound until an exception handler is found to handle the exception.
Fantom does not use Java styled checked exceptions where a method must declare the exceptions it throws. In our experience checked exceptions are syntax salt which don't scale to large projects. For example in Java the real exception is often wrapped dozens of times to make the compiler happy as independent subsystems are integrated.
Err Class
All exceptions in Fantom subclass from Err
. Many APIs declare their own Err
classes for modeling specific error conditions. The type hierarchy is the primary mechanism used to classify exceptions and is used to match exceptions to exception handlers.
All Errs
also have a message and a cause. Message is a human readable Str
to describe the error condition. Cause is another Err
instance which is the root cause of the exception. Cause is often null
if there is no root cause.
Fantom convention requires all exception classes end with Err
and declare a constructor called make
which takes at least a Str
message as the first parameter and an Err
cause as the last parameter. Typically both of these parameters have a default argument of null
.
When an exception is thrown, the runtime captures the call stack of and stores it with the Err
instance. You can dump the call stack of an exception using the trace
method:
err.trace // dumps to Env.cur.out err.trace(log) // dumps to log output stream
Throw Statement
The throw
statement is used to raise an exception via the following syntax:
// syntax throw <expr> // example throw IndexErr("index $i > $len")
The expression used with the throw
keyword must be evaluate to a sys::Err
type. When the throw
statement is executed, the exception is raised and program execution unwinds itself to the first matching exception handler.
Try-Catch Statement
Fantom uses the try-catch-finally
syntax of Java and C# for exception handling:
try { <block> } catch (<type> <identifier>) { <block> } catch { <block> } finally { <block> }
A list of catch blocks is used to specify exception handlers based on exception type. A catch block for a given type will catch all raised exceptions of that type plus its subclasses. If an exception is raised inside a catch block with no matching catch blocks, then the exception continues to unwind the call stack (although if a finally
block is specified it will be executed).
The blocks of code in a try
can be either a {}
block or a single statement. The identifier of each catch block is used to access the exception caught - this variable is scoped within the catch block itself.
You can have as many catch
blocks as you like, however the type of a catch block cannot repeat a type used previously. For example:
// this is legal try {...} catch (CastErr e) {...} catch (NullErr e) {...} catch (Err e) {...} // this is illegal try {...} catch (Err e) {...} catch (NullErr e) {...}
In the first block of code we first declare a catch handler for CastErr
and NullErr
which will catch any exception of those types. We catch Err
last which will catch anything else which is not a CastErr
or NullErr
. However the second block of code will not compile because the NullErr
catch comes after the Err
catch block (which includes NullErrs
).
If you don't need to access the exception caught, you can also use a catch all block to catch all exceptions using the following shorthand syntax:
try { return doSomethingDangerous } catch { return null }
Finally Blocks
We use a finally
block when we want to execute some code which runs regardless of how or if an exception is handled. A finally
block always executes when a try
block exits. Finally is often used to ensure that resources are cleaned up properly no matter what exceptions might occur. For example the following code guarantees that the input stream is closed no matter what might go wrong inside the try
block:
void load(InStream in) { try { // read input stream } finally { in.close } }
Finally blocks have similar restrictions to C#. You cannot exit a finally
block directly using a return
, break
, or continue
statement.