# Chapter 9: Error Handling with Result

The hosted modes have exceptions. Freestanding does not: `throw` and exception `catch` arms are rejected with `E_FREESTANDING_EXCEPTIONS`. This chapter explains why, then shows what replaces them — `Result[T, E]`, the `?` operator, error conversion with `From`, and the `try … attempt … catch Err(e)` block — with the mistakes the compiler catches and two it does not.

## Why there are no exceptions

An exception needs unwind tables (`.eh_frame`), an unwinder that walks them and restores registers, and something sensible to do when nothing catches the exception. A freestanding image has none of these: linking an unwinder would bring back the runtime freestanding exists to avoid, and an interrupt handler on a per-CPU stack has no caller to unwind into.

So freestanding splits errors in two:

- a **recoverable** error is a value — the function returns `Result[T, E]` and the caller decides;
- an **unrecoverable** condition is a panic — the package's panic policy halts the core, faults, or calls your `@panic_handler` (chapter 17).

## Error types

`Result[T, E]` is a built-in enum with the variants `Ok(value)` and `Err(error)`. The error type is yours to choose; usually it is an enum per subsystem:

```loup
@mode(freestanding)

enum UartError {
    Timeout,
    Framing,
    Parity,
    Overrun,
}

enum PciError {
    NoDevice(bus: uint8),
    BadVendor(vendor: uint16),
}

func readByte(lsr: uint8, data: uint8): Result[uint8, UartError] {
    if ((lsr & 0x01u8) == 0u8) {
        return Err(UartError.Timeout)        # nothing received
    }
    if ((lsr & 0x08u8) != 0u8) {
        return Err(UartError.Framing)
    }
    return Ok(data)
}

func describe(r: Result[uint8, UartError]): uint8 {
    return match r {
        Ok(b) => b,
        Err(UartError.Timeout) => 0x00u8,
        Err(e) => 0xFFu8,
    }
}
```

*Listing 9-1: Subsystem error enums and a function that returns `Result`*

## Propagating errors with `?`

`expression?` on a `Result`:

- evaluates to `v` when the result is `Ok(v)`;
- returns `Err(e)` from the enclosing function when it is `Err(e)`.

```loup
#~ @mode(freestanding)
#~
#~ enum UartError { Timeout, Framing, Parity, Overrun }
#~
#~ func readByte(lsr: uint8, data: uint8): Result[uint8, UartError] {
#~     if ((lsr & 0x01u8) == 0u8) { return Err(UartError.Timeout) }
#~     return Ok(data)
#~ }
#~
func readWord(lsr: uint8, hi: uint8, lo: uint8): Result[uint16, UartError] {
    let high = readByte(lsr, hi)?
    let low = readByte(lsr, lo)?
    return Ok(((high as uint16) << 8) | (low as uint16))
}
```

*Listing 9-2: Two fallible reads composed with `?`*

`?` only works where it can return an error. In a function that returns a plain value it is rejected:

```loup,does_not_compile
@mode(freestanding)

enum UartError { Timeout, Framing }

func readByte(): Result[uint8, UartError] {
    return Err(UartError.Timeout)
}

func pollKeyboard(): uint8 {
    let scancode = readByte()?
    return scancode
}
```

```text
error[E_PROPAGATE_WRONG_RETURN]: E_PROPAGATE_WRONG_RETURN: the '?' operator can only be used inside a function returning Result (§1-5.7)
  --> src/main.lpfree:10:20
   |
10 |     let scancode = readByte()?
   |                    ^^^^^^^^^^^
   |
```

`pollKeyboard` has to decide what an error means for its caller. Either return `Result` too, or handle the error where it happens:

```loup
#~ @mode(freestanding)
#~
#~ enum UartError { Timeout, Framing }
#~
#~ func readByte(): Result[uint8, UartError] {
#~     return Err(UartError.Timeout)
#~ }
#~
# 0 means "no key" to the caller.
func pollKeyboard(): uint8 {
    return match readByte() {
        Ok(scancode) => scancode,
        Err(e) => 0u8,
    }
}
```

*Listing 9-3: Handling the error at the boundary with `match`*

## Converting errors with `From`

Higher layers usually want one error type that covers several subsystems. `?` converts automatically when the function's error type implements `From[E]` for the error being propagated. Without the conversion:

```loup,does_not_compile
@mode(freestanding)

enum UartError { Timeout, Framing }
enum KernelError { Uart, OutOfMemory }

func readByte(): Result[uint8, UartError] {
    return Err(UartError.Timeout)
}

func readConsoleId(): Result[uint8, KernelError] {
    let id = readByte()?
    return Ok(id)
}
```

```text
error[E_PROPAGATE_MISSING_FROM]: E_PROPAGATE_MISSING_FROM: '?' propagates an error of type 'UartError' out of a function whose error type is 'KernelError', which does not implement `From[UartError]` (§1-5.7)
  --> src/main.lpfree:11:14
   |
11 |     let id = readByte()?
   |              ^^^^^^^^^^^
   |
```

`From[E]` is an interface with one static method, `from(err: E)`. In 1.25 an enum declaration cannot carry `implements`, so make the kernel-level error a struct that records which subsystem failed and how, and give it one `from` per subsystem error:

```loup
#~ @mode(freestanding)
#~
enum UartError { Timeout, Framing }
enum PciError { NoDevice(bus: uint8), BadVendor(vendor: uint16) }

enum Subsystem { Uart, Pci }

struct KernelError implements From[UartError], From[PciError] {
    var subsystem: Subsystem;
    var code: uint32;

    static func from(err: UartError): KernelError {
        let code = match err {
            UartError.Timeout => 1u32,
            UartError.Framing => 2u32,
        }
        return KernelError(subsystem = Subsystem.Uart, code = code)
    }

    static func from(err: PciError): KernelError {
        let code = match err {
            PciError.NoDevice(bus) => bus as uint32,
            PciError.BadVendor(vendor) => vendor as uint32,
        }
        return KernelError(subsystem = Subsystem.Pci, code = code)
    }
}

func readByte(): Result[uint8, UartError] {
    return Err(UartError.Timeout)
}

func findNic(): Result[uint16, PciError] {
    return Ok(0x8139u16)
}

func initDevices(): Result[uint16, KernelError] {
    let consoleId = readByte()?        # UartError -> KernelError.from(...)
    let nic = findNic()?               # PciError  -> KernelError.from(...)
    return Ok(nic + (consoleId as uint16))
}
```

*Listing 9-4: A kernel error type with `From` conversions*

The compiler inserts `KernelError.from(e)` at each `?` whose error type differs. Low-level drivers keep precise error enums; the glue layer above them speaks one type.

## `try … attempt … catch Err(e)`

`?` sends every error out of the function. When a block of steps should share one local handler instead, use a `try` block whose only handler is a `catch Err(e)` arm. Inside it, `attempt expression` unwraps an `Ok` or jumps to the arm with the error (Language Reference §1-5.10). This form needs no unwinder — it is a `match` and a jump — so it is allowed in freestanding, with an optional `finally`:

```loup
#~ @mode(freestanding)
#~
enum ProbeError { Absent, Timeout }

func probePort(port: uint16): Result[uint8, ProbeError] {
    if (port == 0u16) {
        return Err(ProbeError.Absent)
    }
    return Ok(0x5Au8)
}

var probesRun: uint32 = 0

# Probe two ports; any failure falls back to "no device" (0).
func detect(primary: uint16, secondary: uint16): uint8 {
    var found: uint8 = 0
    try {
        let a = attempt probePort(primary)
        let b = attempt probePort(secondary)
        found = a | b
    } catch Err(e) {
        found = 0u8
    } finally {
        probesRun = probesRun + 1u32
    }
    return found
}
```

*Listing 9-5: A local handler for a sequence of fallible steps*

What is not allowed is the exception form, `catch (Type as e)`:

```loup,does_not_compile
@mode(freestanding)

func decode(scancode: uint8): uint8 {
    return scancode & 0x7Fu8
}

func pollKeyboard(scancode: uint8): uint8 {
    try {
        return decode(scancode)
    } catch (ValueError as e) {
        return 0u8
    }
}
```

```text
error[E_FREESTANDING_EXCEPTIONS]: E_FREESTANDING_EXCEPTIONS: an exception `catch` arm is not available in `@mode(freestanding)` — the mode has no unwinder and no `.eh_frame`. Use `Result[T, E]` with `?` or `attempt` (§1-5.10).
  --> src/main.lpfree:10:7
   |
10 |     } catch (ValueError as e) {
   |       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
```

Nothing in `decode` can fail, so the fix is to delete the `try`; when something can, make it return a `Result` and use `attempt` or `?`. `throw` is rejected the same way (chapter 6).

## Two mistakes the compiler does not catch

**Ignoring a `Result`.** Calling a fallible function as a statement discards its result, and 1.25 does not warn:

```loup,not_desired_behavior
#~ @mode(freestanding)
#~
enum DiskError { Timeout, BadSector(lba: uint64) }

func flushCache(): Result[void, DiskError] {
    return Err(DiskError.Timeout)
}

func shutdown(): void {
    flushCache()          # the error is silently dropped
    # ... power off ...
}
```

*Listing 9-6: A dropped error — compiles without a warning*

If the shutdown path cannot do anything about a failed flush, say so explicitly — `match` and ignore the arm with a comment, or count the failure — so the next reader knows it was a decision.

**`?` on an `Option`.** The Language Reference allows `?` only on `Result` (`E_PROPAGATE_NOT_RESULT`). In 1.25, `?` on an `Option` inside a `Result`-returning function is accepted, and a `None` is propagated as the error value itself — `Err(None)` in a hosted run — not as a value of your error type:

```loup,not_desired_behavior
#~ @mode(freestanding)
#~
enum LookupError { NoSuchIrq }

func slotOf(irq: uint8): Option[int] {
    if (irq < 16u8) {
        return Some(irq as int)
    }
    return None
}

func handlerSlot(irq: uint8): Result[int, LookupError] {
    let slot = slotOf(irq)?          # a None becomes Err(None), not NoSuchIrq
    return Ok(slot)
}
```

*Listing 9-7: `?` applied to an `Option`*

Convert the absence into your error explicitly:

```loup
#~ @mode(freestanding)
#~
#~ enum LookupError { NoSuchIrq }
#~
#~ func slotOf(irq: uint8): Option[int] {
#~     if (irq < 16u8) { return Some(irq as int) }
#~     return None
#~ }
#~
func handlerSlot(irq: uint8): Result[int, LookupError] {
    let slot = slotOf(irq)
    if (slot.is_none()) {
        return Err(LookupError.NoSuchIrq)
    }
    return Ok(slot.unwrap())
}
```

*Listing 9-8: Turning `None` into a typed error*

## Patterns

**Allocation failure.** The `@allocator` returns `uint?`; wrap it once and the rest of the kernel sees a `Result` (chapter 7, Listing 7-5).

**Parsing.** Every step of parsing a header can fail, and the failures compose with `?`:

```loup
#~ @mode(freestanding)
#~
enum ParseError {
    Truncated,
    BadMagic(magic: uint32),
    UnsupportedClass(value: uint8),
}

# The first bytes of an ELF image, as received from a boot module.
struct Image {
    var bytes: buffer[uint8, 64];
    var length: int;
}

func byteAt(img: Image, offset: int): Result[uint8, ParseError] {
    if (offset >= img.length) {
        return Err(ParseError.Truncated)
    }
    return Ok(img.bytes[offset])
}

func checkMagic(img: Image): Result[void, ParseError] {
    let m0 = byteAt(img, 0)?
    let m1 = byteAt(img, 1)?
    let m2 = byteAt(img, 2)?
    let m3 = byteAt(img, 3)?
    let magic = ((m0 as uint32) << 24) | ((m1 as uint32) << 16) | ((m2 as uint32) << 8) | (m3 as uint32)
    if (magic != 0x7F454C46u32) {
        return Err(ParseError.BadMagic(magic = magic))
    }
    return Ok(())
}

func elfClass(img: Image): Result[uint8, ParseError] {
    checkMagic(img)?
    let cls = byteAt(img, 4)?
    if (cls != 2u8) {
        return Err(ParseError.UnsupportedClass(value = cls))   # only ELF64
    }
    return Ok(cls)
}
```

*Listing 9-9: A header parser built from small fallible steps*

`Result[void, E]` is the type of a step that produces nothing but can fail; it returns `Ok(())`.

## Panics: `panic()` and `unreachable`

The Language Reference defines `panic(msg): noreturn` in every mode. The 1.25 Stage0 compiler does not declare it in any mode:

```loup,does_not_compile
@mode(freestanding)

func checkPaging(cr3: uint64): void {
    if (cr3 == 0u64) {
        panic("CR3 is zero: no page table loaded")
    }
}
```

```text
error[E0001]: undefined identifier `panic`
 --> src/main.lpfree:5:9
  |
5 |         panic("CR3 is zero: no page table loaded")
  |         ^^^^^
  |
```

> ⚠ **Planned.** `panic(msg)` is specified (Language Reference §3-3.6) but not available in Loup 1.25.

What does work is `unreachable`: reaching it is a panic stop under the package's `[build] panic` policy — the core halts by default, or your `@panic_handler` runs under `panic = "custom"`, with the source location (chapter 17). Write the message where a person will see it first, then stop:

```loup
#~ @mode(freestanding)
#~
#~ func outb(port: uint16, value: uint8): void {
#~     asm { "out dx, al" in: "dx" = port, "al" = value clobbers: [] }
#~ }
#~
func serialWrite(s: string): void {
    for (c in s) {
        outb(0x3F8u16, c as uint8)
    }
}

func checkPaging(cr3: uint64): void {
    if (cr3 == 0u64) {
        serialWrite("fatal: CR3 is zero, no page table loaded\n")
        unreachable
    }
}
```

*Listing 9-10: Reporting, then stopping with `unreachable`*

When to stop and when to return `Err`:

- **Stop** when an invariant the rest of the program depends on is broken — a corrupt page table, a vector that fired with no handler installed, a state the code has proven impossible.
- **Return `Err`** when the environment failed in a way some caller can handle — a device timed out, a PCI device is missing, a user-supplied length is wrong, a large buffer is unavailable and a smaller one would do.

Low-level drivers return precise errors; the subsystem glue converts them; the top-level loop logs, retries, falls back, or — when nothing else is safe — stops.

Error return traces — the list of `?` sites an error passed through — are captured only in builds made with `--error-traces` (on by default in `--debug` builds); `[freestanding] error_trace_depth` sizes the fixed store (§3-3.21). In other builds, calling `errorTraceDepth()` is `E_ERROR_TRACES_DISABLED`.

## Summary

- `throw` and `catch (T as e)` are `E_FREESTANDING_EXCEPTIONS`; recoverable errors are `Result[T, E]` values.
- `?` returns an `Err` from the enclosing function, which must return `Result` (`E_PROPAGATE_WRONG_RETURN`).
- Implement `From[E]` (one `static func from` per source error) on a struct to let `?` convert errors across layers (`E_PROPAGATE_MISSING_FROM` otherwise).
- `try { … attempt … } catch Err(e) { … } finally { … }` handles a sequence of fallible steps locally and is allowed in freestanding.
- 1.25 does not warn about an ignored `Result`, and `?` on an `Option` propagates `None` as the error — convert explicitly.
- `panic()` is not available yet; report the problem and use `unreachable`, which stops under the package's panic policy.

Next: ownership verbs.
