Exception Handling

Exceptions are the fundamental way how Elements deals with errors, in a consistent way that works the same across all platforms, and works similarly, for all languages.

Exceptions happen asynchronously from the regular application flow. When an error occurs, an exception is "raised" (in Oxygene parlance) or "thrown" (in C#, Swift and Java parlance), and it interups the current execution flow at that point. The exception travels back up the call stack of methods, until it is handled (or "caught") by an exception handler, or until it reaches the top of stack.

Exceptions can be caught explicitly, by code in your project, or implicitly, by the surrounding platform or application framework (such as Cocoa, WinForms, or WPF) that provides the execution context for your app.

If an exception reaches the top of its stack, uncaught, it will usually result in termination (or "crash") of your application.

As an exception travels up the stack, each stack frame gets the chance to interact with it, to perform cleanup, save release of resources, error logging, or to catch the exception.

How Exceptions Happen

There are typical three sources for an exception:

  1. Your code explicitly raises an exception, because it detected an error condition, using the raise (Oxygene) ot throw (C#, Swift of Java) keyword.
  2. Your code performed an action that lead the compiler or runtime to generate an exception (such as a null reference access, or a bad cast).
  3. Your code called a framework. platform, or third party library API that in turn caused an exception.

The subsequent behavior is identical, in all three cases. In the last case, the Call Stack of the exception might contain further details about the original source of the error.

Raising/Throwing Exceptions Manually

If your code deems it necessary, it can manually raise an exception, using the raise (Oxygene) ot throw (C#, Swift of Java) keyword:

if i > 5 then
  raise new Exception("Did nit expect a value larger than five!");
if (i > 5)
  throw new Exception("Did nit expect a value larger than five!");
if i > 5 {
  throw Exception("Did nit expect a value larger than five!")
}
if (i > 5)
  throw new Exception("Did nit expect a value larger than five!");

While not the most common case for exceptions, this is useful for when your code detects error conditions that it is not prepared to handle. Raising na exception is a convenient way of putting the onus for the bad input on the caller, rather than dealing with bad data at the current level of your algorithm, for example.

The higher levels of your app might be better equipped to handle the error, depending on its cause – for example, if it was caused by bad user input, you might just want to display an error message to the user, but if the exception was caused by faulty logic on the higher level, the exception will let you know that a bug fix is needed there.

Catching Exceptions

If you expect certain sections of code to throw exceptions, sometimes it makes sense to explicitly catch and deal with the error at the right level. For example, when writing data to a disk, an "expected" cause for an exception could be that the disk is full, or the file is locked.

Each language provides mechanism for catching exceptions, either those of a certain type (e.g. only errors related to network access) or more widely all exceptions:

  • Oxygene: try/except Blocks
  • C#: try/catch Blocks
  • Swift: do/catch Blocks
  • Java: try/catch Blocks

Except on the very highest level of your application, it makes usually sense to catch only those types of exceptions you expect, and know how to deal with at that level, and let all other exceptions bubble further up. For example, in a library that downloads a file from the network, you might reasonably expect network errors, or a "disk full" error, and have the means to react to those (maybe retry the download, or fail with a new, more high-level error), but you will probably have very little means to deal with, say, a null reference exception, or an out of memory condition – so it will be best to let those pass through.

Depending on the type of your application, you might want to catch all exceptions and the top level (to prevent the user from seing an ugly crash or have your app just disappear), but you should also keep im mind that some truly unexpected exceptions might leave your app in an inconsistent state. Sometimes it is better to show the user a n ice error and then quit, rather then keep running with bad data, and destroy the user's document in the process.

try
  // this might raise an Exception
except
  on E: IOException do
    // handle error
end;
try
{
    // this might raise an Exception
}
catch (IOException e)
{
    // handle error
}
do {
    // this might raise an Exception
} catch IOException {
    // handle error
}
try
{
    // this might raise an Exception
}
catch (IOException e)
{
    // handle error
}

Protecting Code from Exceptions, and Cleaning Up

Some methods will need to perform cleanup or housekeeping when an exception, any exception, occurs, even if the code does not deal with the exception itself.

For example, your code might open a file, read and process its data, and then close the file. Any number of (unexpected) errors could occur during the reading or processing – but you would still need to make sure the file gets closed properly, else it might stay locked, or lose data.

Again, each language has a construct for this:

  • Oxygene: try/finally Blocks
  • C#: try/finally Blocks
  • Swift: defer or __finally Blocks
  • Java: try/finally Blocks

Code in a finally (or defer, for Swift) block will run regardless of whether an exception occurred or not, making it perfect for cleanup code that you want to run both on success and on failure.

var file := OpenFile(...);
try
  // this might raise an Exception
finally
  file.Close();
end;
var file = OpenFile(...);
try
{
    // this might raise an Exception
}
finally
{
    file.Close();
}
let file := OpenFile(...)
defer {
    file.Close()
}
// this might raise an Exception
var file = OpenFile(...);
try
{
    // this might raise an Exception
}
finally
{
    file.Close();
}

using statements are a convenient way to clean up resources that implement the Disposable pattern, as they essentially combine a finally/defer with a call to a standardized clean-up method:

  • Oxygene: using Blocks
  • C#: using Blocks
  • Swift: __using Blocks
  • Java: "try with Resource" Blocks

Common Exception types

Each platform and library has its own set of standard exceptions for errors that occur when working with the platform. We recommend to familiarize yourself with the platform and/or the third party libraries you use to see which exceptions you might need to handle ("catch" and deal with) in your code.

By convention, all exception types descend from the root Exception base class, available unde the same name on all platforms. You can use this type (or a type-less except or catch clause) if you want to catch all exceptions – which is recommended only on the very highest level of your app.

There are a couple of standard exception types you will across all platforms that are generated by the compiler itself, for invalid cde patterns.

  • A Null Reference Exception will occur if you try to access member of an object on a variable that has not been initialized yet, and contains nil or null, or when you try to pass a nil or null value top a parameter that does not accept nullable values.

  • An Invalid Cast Exception will occur if you try to cast or forcibly assign an object reference to the type it is not compatible with (e.g. if you try to cast a String to a Button).

Stack Traces

Exceptions typically contain a stack trace that gives you information of the different levels of method calls your application was in, at the time the exception occurred. The stack trace (along with other parameters of the exception, like its Message) can be very useful to narrow down where an exception occurred, even when it was called (and maybe logged for diagnosis) at a higher level of your app.

If your project uses Elements RTL, a property called CallStack is available consistently across all platforms to give you access to the call stack for the exception.

See Also