ajhahn.de
← Flash
Markdown 147 lines
# Chapter 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:

```flash
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. 

```flash
// 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:

```flash
// 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.

```flash
// 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:

```flash
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:

```flash
// 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:

```flash
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.

```flash
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:

```flash
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`:

```flash
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.