Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Say which operation failed

IoError.System(2) says a file was missing, but not which file or which step wanted it. std.error wraps an error in a Context holding an operation label and, optionally, a path. The original error stays inside as cause, with its type.

from std.error import Context, Subject, context, path_context, native_code
from std.path import Path, from_text

def read_limit(path: &Path) -> Result[list[u8], Context[IoError]]:
    path_context(read_bytes(path.as_bytes()), "read limit", path)

def parse_limit(text: &str) -> Result[i64, Context[ParseError]]:
    context(i64.parse(text), "parse limit")

def flush_output() -> Result[(), Context[IoError]]:
    context(flush_stdout(), "flush output")

def limit(path: &Path) -> Result[i64, AllocError | IoError | Context[ParseError]]:
    match read_limit(path):
        case Ok(bytes):
            Ok(parse_limit(&decode_utf8(bytes)?)?)
        case Err(error):
            print(error.describe()?)?
            print(native_code(error.cause))?
            Ok(10)

def run() -> Result[(), AllocError | IoError | Context[IoError] | Context[ParseError]]:
    path = from_text("limit.txt")?
    print(limit(&path)?)?
    match parse_limit("ten"):
        case Ok(value):
            print(value)?
        case Err(error):
            print(error.describe()?)?
            print(error.cause == ParseError.Invalid)?
    flush_output()?
    Ok(())

def main() -> i32:
    match run():
        case Ok(_):
            0i32
        case Err(cause):
            Context("run", Subject.Absent, cause).report()
            1i32
read limit "limit.txt": IoError.System(2)
Option[i32].Some(2)
10
parse limit: ParseError.Invalid
True

context(result, label) and path_context(result, label, &path) pass an Ok through and wrap an Err, including operations whose success is (), as flush_output shows. main uses the direct form, Context(label, Subject.Absent, cause). Nothing is wrapped implicitly: ? needs a compatible declared error, so read_limit returns Context[IoError] and callers can include that context in an error sum.

describe() builds one line: the label, the quoted path if there is one, and the cause. It allocates, so it returns a Result. error.cause is the untouched typed error; match it, compare it, or ask native_code for the OS number.

report() is for the edge of the program. It flushes standard output and writes error: plus the description to standard error. It returns nothing and does not choose an exit status, so main returns i32 and decides. If memory runs out while describing, report still writes the label with a fixed message, and a failed path copy in path_context keeps the label and cause.

Here run preserves its allocation, I/O, and contextual errors in a sum. The outer context adds the operation label without discarding the cause. Return a Context or your own enum when a failure needs more explanation. See the module reference for nesting contexts and the rules standard modules follow.

For a human-readable cause, give its type the public std.report formatting method. Contexts then format their operation labels and delegate to that cause:

from std.error import Context, Subject
from std.fmt import Formatter, FormatError
from std.report import to_string

pub enum SettingsError:
    Invalid

    pub def format[W](self, output: &mut Formatter[W]) -> Result[(), FormatError]:
        output.text("invalid setting")

def main() -> Result[(), FormatError | IoError]:
    inner = Context("parse", Subject.Absent, SettingsError.Invalid)
    outer = Context("load settings", Subject.Absent, inner)
    print(to_string(&outer, 120, 8)?)?
    print(to_string(&outer, 120, 8)?)?
    Ok(())
load settings: parse: invalid setting
load settings: parse: invalid setting

Formatting borrows both contexts and includes the cause once. Returning an Err with this context type from main selects the same wording automatically, with the entrypoint’s prefix and newline. A context whose cause has no formatting method still uses structural entrypoint reporting. The existing describe, describe_with, report and report_with methods keep their behavior.

Without a path, context formatting adds no allocation. A present path still uses fallible Path.display; failure leaves the typed cause intact and may leave part of the report already written. See bounded reports for limits and failure handling.