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.