The error channel is opt-in¶
Status: SHIPPED. This is the decision record. The rulings are David's (2026-09-30) and are settled.
The rule¶
A callable has an error channel only when its signature writes | E, or when it returns
an explicit Result@(T, E). The rule is the same for a free function, an extension or
perk method, a lambda and a function type. There is no default error type.
use <collections/strings>
fn parse(string text) i32 | StdError: # a channel: the call yields Result@(i32, StdError)
if (text.is_empty()):
return Result.Err(StdError.Error)
return Result.Ok(text.len())
fn double(i32 x) i32: # bare: the call yields i32
return x * 2
- A body with a channel spells both constructors:
return Result.Ok(x)andreturn Result.Err(e). A barereturn xthere is CE2030. - A BARE body returns the value.
return Result.Ok(...)andreturn Result.Err(...)are CE2091, and a??is CE0131. A bare function that calls a fallible one handles the error itself (match,.realise(default)), or its signature writes| E. - The call of a bare function yields the value.
??on it is CE2507, and.realiseis CE2008, because the value is not aResult. - A body that answers a value, or a Result, ends in a
returnon every path (CE0107). A bare~body answers nothing and may reach its end. mainis bare. It returns the exit code (return 0), and| Eon it is CE0106. A??inmainis CE0131.- A lambda has a channel only when its TYPE says
| E. The type comes from the annotation on the lambda (|i32 x| -> i32 | E: ...) or from the expected type (a parameter, alet, a field). The compiler never infers a channel from the body. An expression body isreturn ewhen the lambda is bare, andreturn Result.Ok(e)when it has a channel. A??in a lambda with a channel is legal inside any body, a bare one included (#399). - A function type without
| Eis bare:fn(i32) -> i32. With it,fn(i32) -> i32 | E. The channel is part of the type, so the two do not convert (CE2002). - A function with a channel that returns a FUNCTION TYPE writes the explicit form,
fn make() Result@(fn(i32) -> i32, StdError):. A| Ewritten after a function type belongs to that function type, sofn make() fn(i32) -> i32 | StdError:is a bare function that returns a function type with a channel. - The combinators of
<collections/iter>(map,filter,fold,compose) are bare: they take bare functions and yield the value. - A perk contract method is bare or has a channel. CE0133 checks that an implementation agrees.
A bare function is the exception¶
Use a bare function seldom, and only when it is needed: the function is total over its
inputs and will stay so (a checksum, a pure arithmetic or string helper, a path join),
and a channel would only force a dead .realise or ?? on every caller.
Write a channel for everything else: a function that does I/O, parses, allocates on a
size it is given, or can gain a failure later. The reason for a public function is the
ABI: a channel added later changes the signature, and that breaks every caller and every
binary .slib. A public function keeps a channel when there is any doubt. The compiler
does not enforce this rule; there is no lint.
Traps are not errors¶
A runtime error (RE2020 for an index out of bounds, RE2021 for a failed allocation, the
other RExxxx codes) is a defect, not an error. It stops the program. A bare function
can trap exactly as a function with a channel can, and a channel does not catch a trap.
Why¶
One rule for every callable gives one field one meaning: err_type of None is "bare" on
a function and on a method alike. A default error type makes every caller of a total
function write a dead ?? or .realise(0), and it gives main an error channel that
exits 1 and prints nothing.
The precedent is the same in the languages Sushi takes its error model from: a Rust fn
returns T unless it writes Result<T, E>; a Swift function throws only when it writes
throws; a Zig function fails only when its return type is an error union E!T; an Elm
function returns Result only when its type says so.
Rejected options¶
- A marker for the bare form, with a default error type (
fn f() u32 | never). It keeps the default that forces the dead unwrap, and adds a second spelling. - An uninhabited error type (Swift
throws(Never), RustInfallible). A Result that cannot fail still has the Result ABI and still needs an unwrap at every call. - Inferring "cannot fail" from the body. An edit to the body would then change a
public signature, and the ABI of a binary
.slib, in silence.
The one predicate¶
has_channel (sushi_lang/semantics/channel.py) is the one question, and every reader
asks it: the collect pass (CE0131, CE2085), the typecheck pass (the body state, the
return rule, the ?? channel, CE0107, what a call yields), the lift pass (the desugar of
an expression lambda), and the backend (channel_result_of, which declares a callable
with its channel Result, its bare return, or void). The .slib manifest states
has_channel for every function, helper and method record. The templates schema is 8
and the container version is 5. A library with an older schema or container version is
refused (CE3512, CE3509) and never read as bare.