ajhahn.de
← all chapters

Flash tutorial · 9 / 12

9. Error Handling

Flash treats errors as values rather than throwing exceptions, using Optionals for missing values, Error Unions for failures, and Defer blocks for cleanup.


1. Optionals (?T)

An optional type represents a value that might either exist (T) or be empty (null).

  • Spelling: ?usize or ?[*]u8
  • Unwrapping: Use orelse to specify a fallback value:
var name ?cstr = get_username()
// Fallback to "guest" if name is null
final_name := name orelse "guest"

2. Error Unions (!T)

An error union represents a value that is either a success value (T) or an error code.

// A function that returns a u32 OR an error code
fn parse(input []u8) !u32 {
    if input.len == 0 {
        return error.EmptyInput
    }
    // ...
}

Named Error Sets (error{…} and E!T)

The bare !u32 above lets the compiler infer the error set. You can instead name a set explicitly with error{…} and join it to the success type with the infix E!T form:

// A named set of the errors this routine can raise
const ParseError = error{
    EmptyInput,
    Overflow,
}

// The return type names the set explicitly: a ParseError, or a u32
fn parse(input []u8) ParseError!u32 {
    if input.len == 0 {
        return error.EmptyInput
    }
    // ...
}

An individual error value is originated with error.Name. The optional (?T) and error-union (!T) markers compose, so a routine can return ?u32 (maybe a value), !u32 (a value or an error), or !?u32 (an error, or maybe a value).

Try and Catch

  • try: Evaluates an expression. If it is an error, the function immediately returns that error. If successful, it evaluates to the unwrapped value.
  • catch: Handles an error inline and evaluates to a fallback value or code block.
// Propagate the error up if read fails
const bytes = try file.read()

// Handle the error inline, falling back to 0
const size = file.read() catch 0

Catch with Error Capture (catch |err|)

You can capture the specific error code using the catch |err| syntax to inspect the error inside a fallback block:

const size = file.read() catch |err| {
    if err == error.PermissionDenied {
        log("Access denied!")
    }
    return 0
}

The capture is optional. When the recovery does not need the specific error, expr catch { … } runs the block on any error without binding it:

// Best-effort flush: ignore any error and carry on
device.flush() catch {}

Error Capture on if and while (else |err|)

When the condition of an if (or while) is an error union, the success payload binds in the usual |value| capture — and the else arm can bind the error with else |err|, completing the capture surface catch |err| opened:

if file.read() |bytes| {
    process(bytes)
} else |err| {
    log_error(err)
}

A for loop's else arm takes no capture — there is no error to bind — and a stray one is rejected with a guiding diagnostic.


3. Resource Cleanup (defer / errdefer)

To prevent memory leaks and ensure resources (like file handles) are closed, Flash inherits Zig's defer blocks.

  • defer: Schedules code to be executed when exiting the current block scope, regardless of how the block is exited.
  • errdefer: Schedules code to be executed only if the block exits with an error.
use flibc

fn copy_file(src_path cstr, dest_path cstr) !void {
    const src_fd = flibc.sys.open(src_path)
    if src_fd < 0 { return error.OpenFailed }
    // Ensure file is closed when function returns
    defer _ = flibc.sys.close(src_fd)
    
    const dest_fd = flibc.sys.open(dest_path)
    if dest_fd < 0 { return error.CreateFailed }
    defer _ = flibc.sys.close(dest_fd)
    
    // Copy logic...
}

Block Form (defer { … })

Both keywords also accept a brace-delimited block, for deferring a sequence of statements. The block opens its own scope and runs whole on exit:

defer {
    _ = flibc.sys.close(fd)
    log("closed")
}

Error Capture on errdefer (errdefer |err|)

An errdefer can see which error is unwinding, with the same capture-pipe shape as catch:

errdefer |err| log_failure(err)

errdefer |err| {
    log_failure(err)
    cleanup()
}

The capture binds for the deferred code only and by value (errdefer |*err| is rejected, like every other error capture). A capture pipe on a plain defer gets its own targeted diagnostic — there is no error on a normal exit.