std.error — explicit mapping and contextual errors
Import std.error. A Context[E] records which operation failed and, when
there is one, the path it acted on, around an unchanged typed cause E. The
module is ordinary embedded Plenty code over Result
and std.path; it adds no exceptions, implicit conversions, or
runtime support.
pub enum Subject:
Absent
At(Path)
Unavailable(AllocError)
pub class Context[E]:
pub operation: str
pub subject: Subject
pub cause: E
| Operation | Result | Contract |
|---|---|---|
map_error(result, mapper) | Result[T, G] | Consumes Result[T, E]; invokes an owned OnceCallable[[E], G] only on Err; same as the result.map_error(mapper) method |
Context(operation, subject, cause) | Context[E] | Takes ownership of all three; allocates nothing |
context(result, operation) | Result[T, Context[E]] | Wraps an Err with Subject.Absent; allocates nothing |
path_context(result, operation, &path) | Result[T, Context[E]] | Wraps an Err, copying the path only then |
error.describe() | Result[str, AllocError] | One-line text ending in str.repr of the cause |
error.describe_with(&cause_text) | Result[str, AllocError] | The same text with a caller-supplied cause |
error.format(&mut output) | Result[(), FormatError] | Borrowed std.report formatting when E satisfies Formattable |
error.report() | () | Writes error: and the description to standard error |
error.report_with(description) | () | The same for a description Result the caller built |
native_code(error) | Option[i32] | The OS code of IoError.System(code); otherwise Nothing |
context and path_context consume the Result and return Ok payloads
unchanged, including () from operations such as flush_stdout() and
File.close(). Conversion is always this explicit call: ? neither wraps an error
in a Context nor unwraps one, and the
identical-error rule
applies to Context[E] as to any error type. A Context moves, because it can
own a path. Its fields are public: read error.cause or match it to recover the
typed cause, at any nesting depth. ? in a function returning
Result[T, Failure], the last-resort erased error, discards a Context like any
other error; list Context[E] in the sum to keep it.
from std.error import Context, path_context
from std.path import Path, from_text
def read_settings(path: &Path) -> Result[list[u8], Context[IoError]]:
path_context(read_bytes(path.as_bytes()), "read settings", path)
def main() -> Result[(), AllocError | IoError]:
path = from_text("settings.toml")?
match read_settings(&path):
case Ok(bytes):
print(len(bytes), "bytes of settings")?
case Err(error):
# A missing settings file is expected: report it and use defaults.
print(error.describe()?)?
print(error.cause == IoError.System(2i32))?
print("using default settings")?
Ok(())
read settings "settings.toml": IoError.System(2)
True
using default settings
Explicit error mapping
result.map_error(mapper) consumes Result[T, E] and returns Result[T, G],
where mapper is an owned OnceCallable[[E], G]. A one-payload enum variant is
the usual mapper; use a named classifier when several error variants need
different policies, or a closure to add other payloads or captured context:
i64.parse(text).map_error(ConfigError.Parse)?
print(line).map_error(def [path](cause: IoError) -> AppError: AppError.Write(path, cause))?
The library function
map_error[T, E, G, F: OnceCallable[[E], G]](result: Result[T, E], mapper: F)
has the same behavior; the method is a compiler-provided Result method, not
an import. A class or enum that defines its own map_error keeps it.
The receiver and mapper expressions run once, receiver first, even on success.
The mapper runs exactly once on Err, taking ownership of the original error;
on Ok, the success payload moves through unchanged and the unused mapper is
dropped. Named functions, reusable closures (including mutable environments),
and consuming closures are accepted. Captures and affine payloads follow normal
cleanup rules, including early return while constructing the mapper.
Mapping adds no allocation or boxing. Callback construction, execution, and
destruction retain their ordinary effects. If the callback returns
Result[G, X], the output is Result[T, Result[G, X]]: nothing is flattened
and no cause-preservation policy is invented. Use context
or path_context when their explicit context-allocation policy fits.
? still requires identical error types (except legacy Failure erasure);
the conversion is visible at the map_error call. The mapper must be owned and
take exactly the error type: a reference, a variant with several payloads, or
a callable returning unit is rejected. .map_err(...) is not implemented.
See the lesson.
Worker-reachable calls accept thin named or anonymous function values,
including variant constructors such as ConfigError.Parse, when every function
address of that callable signature in the program passes
worker-effect certification.
The compiler checks every candidate body and its stored types; one safe target
never excuses an unsafe one. A main-thread-only address with the same signature
can conservatively prevent certification. A concrete closure retains its own
body and capture evidence; it cannot bypass the checks either.
Allocation failure
Attaching context never replaces the cause. path_context copies the borrowed
path only on Err; if that copy fails, the context holds
Subject.Unavailable(alloc_error) with the original operation and cause. To
avoid the copy, move an owned path into Context(operation, Subject.At(path), cause).
The Ok path of both helpers, construction, and field access allocate nothing.
describe allocates the cause’s representation, the escaped path, and
intermediate strings, and returns AllocError if any step fails. The context
is unchanged either way.
Description format
operation: cause
operation "escaped path": cause
operation <path unavailable>: cause
The path uses Path.display: printable ASCII is kept, backslash and
double quote are escaped, and every other byte is \xhh, so a description is
one line of ASCII apart from the operation and cause. describe renders the
cause as str.repr does; a native
code appears as IoError.System(2).
A nested context is a cause like any other, so describe shows its structural
representation. Describe the inner context first and pass the text outward:
outer.describe_with(&outer.cause.describe()?)
That gives load settings: read_bytes "missing/settings": IoError.System(2).
An application error enum can match its variants and call the method that fits
each payload. std.fmt.Formattable provides borrowed formatting
for application types. Context[E] supports this protocol when E does:
it emits the operation and subject, then calls render on the typed cause
exactly once. Nested contexts therefore produce nested operation labels without
duplicating the cause. Each nested render counts toward the depth limit.
This conditional method is compiler-supported for the embedded std.error
type; it does not introduce conditional methods for arbitrary generic classes.
An unformattable cause keeps the existing description APIs and structural
entrypoint reporting. describe and describe_with retain their existing
behavior even when the cause has a format method.
Formatting an absent or unavailable subject adds no allocation. A present path
uses the allocating Path.display; failure is returned as
FormatError.Allocation(...), preserving the cause and any
already-written prefix. Operation and cause text are emitted unchanged.
Reports
report flushes standard output, then writes error: , the description, and a
newline to standard error in one write. It returns unit: flush and write
failures are dropped, because standard error is the last channel. The caller
keeps the error and chooses the exit status, normally by returning i32 from
main.
Neither fallback allocates:
| Condition | Standard error |
|---|---|
| The description cannot be allocated | error: operation: no memory to describe the error |
| The description exists but the whole line cannot be allocated | The normal line, written in three pieces |
report_with(description) applies the same rules to a description the caller
built, such as the nested one above; pass the Result unhandled.
Returning Err(context) from main produces the
entrypoint report. It selects the
typed formatting method when the cause is formattable, otherwise renders the
context structurally and shows a path as its byte list. The explicit report
and report_with APIs keep their description-based behavior.
Conventions for standard modules
- An operation returns its precise error type:
IoError,AllocError, or the module’s own enum. It does not attach context, format text, or allocate to describe a failure. - A module enum that can fail from allocation has a variant carrying the
AllocError, asWalkError.AllocationandDataError.Allocationdo. Allocation failure is never folded intoOtherorFailure. - A native code is kept as
IoError.System(code)and never translated to text in the error value. - A composite operation that must say which step or path failed returns
Context[E], or an enum whose variants hold contexts. The operation label is a string literal naming the public operation, such asread_dir. - Failing to allocate context reports the original cause, with
Subject.Unavailable. - Only an application decides to print. Library code returns errors;
reportbelongs at the boundary that owns the exit status.
WalkError.Io predates this module and keeps its own
(path, operation, cause) payload.
Limits
describeandreportrequire a cause typestr.repraccepts. A cause holding recursive data or a generator is rejected where one of them is first called for thatContext[E].- No OS description text is added to a native code.
- The legacy
reportandreport_withmethods usewrite_stderr: a pipe whose reader has gone terminates the process withSIGPIPE. Thestd.reportformatter and entrypoint reporter suppress it and preserve normal cleanup. - A piecewise fallback report is not atomic against concurrent writers.
Subjecthas no network address or other non-path variant.
A Context[E] can cross threads when E can. Every operation takes time linear
in the text it produces. Supported target: x86_64 Linux GNU. The lesson is
Say which operation failed.