Error handling

All functions in Leaf can raise errors. A tagging mechanism provides information about the error. To avoid forgotten handling, errors are automatically propagated up the call chain until something knows how to deal with it. Error handling has several forms to reduce syntactic overhead.

defn pine = (x : integer) -> {
    x < 0 then fail string_tag("invalid-x")
    return x +1
}

defn cone = (y : integer) -> (q : integer) {
    q = pine(y) on fail {
        std.print( "oh well" )
        return 0
    }

    return q
}

fail and on_fail

An error starts with the fail command. An optional tag may be provided to give information about the error (we'll get bag to tags a bit later).

person.is_jedi() else fail tag_string("no-power-here")

A caller intercepts errors using the on fail syntax, which handles the error and returns to normal program flow.

do {
    var user = get_primary_user()
    user.equip_jetpack()
} on fail {
    std.print( "No flying today" )
    return
}

The full do ... on fail syntax can be verbose at times, so on fail can be added to individual statements as well.

var user = get_primary_user() on fail {
    std.print( "Sneaky person can't be found")
    return
}

user.equip_jetpack() on fail {
    std.print( "User has no jetpack" )
    return
}

It'd be reasonable to wonder about the safety of having a var user = ... on fail. If we capture an error during initialization or assignment, we'd be left with an undefined value in user. The compilation will fail in such cases, indicating you cannot continue with normal flow.

fail_info and current_fail

Failures can carry information with them. If propagating through multiple levels, additional information can be added.

var tag_invalid_path = string_tag( "invalid-user-path" )

defn load_user = ( file_name : arraychar ) -> {
    var file = sys.file.open_read( file_name ) on fail {
        current_fail.add( tag_invalid_path )
        resume_fail
    }

    return parse_user( file.read_text() )
}

current_fail is the error structure for the current error. The .add function adds extra information to it. A caller can inspect this information.

load_user( user_file ) on fail {
    current_fail.has( tag_invalid_path ) then {
        std.print( "Could not locate user file" )
    } else {
        std.print( "Uncertain what happened" )
    }
    return
}

We used the variable tag_invalid_path to look for the data. Tags are unique and must be compared this way. Though this one refers to a string, another tag with the same string is not the same. This mechanism will be extended to include more than just strings once variant types are added.

Extra error data is meant to be informational only. It's discouraged to change the processing significantly depending on what type of error has happened. There are exceptions to the rule, but in general, we don't want tags to be used as a flow control mechanism.

trap

Sometimes it's more convenient to trap an error in a variable than write an fail handler. For this, we have the trap command. It bundles up the value or failure of an expression.

defn get_suitable_user = -> {
    var user = trap get_user()
    has(user.error) then std.print( "using dummy" )
    return user.value | get_default_user()
}

This example shows both the error and value fields of the trap structure. In practice, we suspect code would only use one of the values. As we create more code, we'll adapt and extend the syntax.

defer_error and current_fail

current_fail is also available within a defer_error: a block that executes when an error propagates out of the surrounding scope. This structure gives us a way to extend the error with more information.

defn parse_file = ( file_name : arraychar ) -> {
    defer_error {
        current_fail.add( string_tag( "failed-parsing-file" ) )
    }

    ...