10. Error handling

Like many other languages, Mojo has built-in support for error handling. In Mojo, an error is raised using the keyword raise and handled using except. Any function call that can potentially raise an error must be wrapped within a try and except block.

The following diagram illustrates the structure of error handling in Mojo.

Error

The following example shows how a function declares that it raises an error and how the caller of a function handles it.

fn raise_error(cond: Bool) raises:
    if cond:
        raise Error("Provided condition is True") 
    else:
        print("No error")

Usage:

    try:
        raise_error(True)
    except e:
        print("Error raised:", e)

In the example, an error is raised inside the function raise_error based on a given condition. Since an error is raised, the function must declare that in its signature. This means that any one calling the function must either handle the error raised by the function, or must re-raise it further down. In the usage example, we see that the error raised by the function is handled.

It is possible to raise a String as error as shown below, though in reality it automatically gets wrapped within an Error.

fn raise_str_error(cond: Bool) raises:
    if cond:
        raise "String error is allowed"
    else:
        print("No error")

Usage:

    try:
        raise_str_error(True)
    except e:
        print("Error raised:", e)

10.1. Error propagation

In the following example, we see that any function that calls another function that potentially raises an error, needs to either fully handle the error within the function itself, or declare in its own signature that it too raises an error. Unhandled errors in a called function will get propagated down.

fn raise_call() raises: # Need to either have 'raises' in the signature, or wrap with try-except
    raise_error(True)

10.2. Finally

The finally code blocks always gets executed regardless whether an error is getting propagated, or it was fully handled. The finally block is typically used for clean-up activities, for example, if a file is opened within a try block, then we must close the file within a finally block to ensure that even if an error is raised within the try block the file is always closed before the function returns. If a value is returned in finally and try or except, the returned value will be from finally. The finally block is optional.

    try:
        raise_error(True)
    except e:
        print("Error raised:", e)
    finally:
        print("Always executed")
    try:
        raise_error(False)
    except e:
        print("Error raised:", e)
    finally:
        print("Always executed")

10.3. Else

In order to execute statements when no error has been raised, you can use else block. The else block is optional. The else block appears after except, but before finally. The else block is useful for those cases where we want to isolate error raising functions that we want to handle from other code that we want to execute, but may raise their own errors.

    try:
        raise_error(False)
    except e:
        print("Error raised:", e)
    else:
        print("No exception raised.")