Error types and error conversion¶
Status: ACCEPTED and BUILT. The rulings are David's (2026-10-04). Everything in sections 2
to 8 is built, the stdlib uses it, and section 9.1 (the from marker) stays deferred.
Section 1 and the "Before" examples of section 6 describe the language before this work
(at 82d6bf94).
Summary¶
- An error type is declared with
error. It is an enum with a flag, and it is ordinary data in every position. - The
Eof everyResult@(T, E)in the program is an error type. extend FileError as IoError:declares a conversion from one error type to another.e as IoErrorcalls it explicitly, and??calls it when the two error types differ.??takes aResult@(T, E)and nothing else. On aMaybe,m.or_err(nom e)??is the form.r.map_err(f)??converts at one site, with no declaration.
The rulings¶
| # | Ruling | Section |
|---|---|---|
| E1 | An error type is declared with the keyword error. It is an enum with an is_error flag, not a new type kind |
2.1, 2.2 |
| E2 | An error type is ordinary data in every position | 2.3 |
| E3 | The E of every Result@(T, E) in the program is an error type. CE2084 carries the rule, and CE2086 is retired |
2.4 |
| E4 | E3 is judged at every written type and at each generic instance, not at the Result interning seam |
2.5 |
| C1 | A conversion is a declaration: extend <Source> as <Target>: with a block body |
3.1 |
| C2 | e as <Target> is the explicit use of a conversion. as consumes its operand by position, unmarked |
3.2 |
| C3 | ?? calls the declared conversion when the two error types differ |
3.3 |
| C4 | A conversion is one step. A chain is never followed | 3.4 |
| C5 | Only the unit that declares the target type may declare a conversion into it | 3.5 |
| C6 | The source and the target are non-generic error types | 3.6 |
| C7 | The body is bare, and it consumes self |
3.7 |
| C8 | ?? takes a Result@(T, E) only. A Maybe and an enum shaped like a Result or a Maybe are refused. or_err(nom e) is the form for a Maybe (#1168) |
4 |
| C9 | or_err and map_err are built-in methods, beside .realise() and .err(), and need no import. map_err ships in the first version |
4, 5 |
| C10 | A from marker on a variant is deferred |
9.1 |
| C11 | A conversion has no leak check | 3.8 |
| C12 | No unit may declare a conversion into StdError, because StdError has no home module |
3.5 |
| C13 | An identity cast x as T on a value of type T stays legal and does nothing to the value. It consumes its operand by position, as every as does (C2): a value that owns a resource is spent |
3.2 |
| C14 | A conversion body that casts a value of its own source into its own target calls itself, and is refused (CE2523). A cast of another pair in the body is legal | 3.4 |
1. Motivation¶
1.1 "Enum" is a substitute for "error"¶
The E of T | E and of Result@(T, E) must be an enum (#668). CE2084 refuses a
struct, a primitive, an array and a function type. CE2086 refuses Maybe and Result.
The rule is wrong in two directions:
- It admits a type that is an enum by representation and not by intent.
Maybe@(T)andResult@(T, E)are ordinary interned enums, so CE2086 refuses them BY NAME. A future built-in wrapper is not on that list. - It admits an enum that is not an error vocabulary.
| Coloris legal.
1.2 ?? needs one error type¶
?? propagates only when the two error types are the same type. Otherwise it is CE2511,
and its doc text ends with "Error type conversion is not supported yet." A function that
calls two modules with two error types cannot use ?? on both. Each call to the other
module is a match, and match is a statement, so each such call nests one level
deeper. At 82d6bf94, the stdlib has 14 of these matches (10 in io/fs.sushi, 4 in
net/tcp.sushi), each of the form
Result.Err(e) -> return Result.Err(e.to_io()), and two conversion methods
(FileError.to_io(), NetError.to_io()). A user program has no shorter form.
1.3 ?? on a Maybe invents an error¶
In a body with | E, ?? on a Maybe that holds None returns Result.Err built from
undef (#1168). The caller reads an E that the program never made, and the answer
changes with --opt.
1.4 ?? reads the shape of its operand¶
?? accepts any enum with Ok/Err variants or Some/None variants, not only
Result and Maybe (_unwrapped_arms, passes/types/expressions.py; the CE2507 text
says "result-like enum"). A user enum Outcome: Ok(i32), Err(E) propagates through
??. The Err payload of such an enum is not a Result, so no rule on error types
reaches it, and a user enum with Some/None has the fault of 1.3.
2. Error types¶
2.1 The declaration¶
error ConfigError:
Unreadable(FileError)
Empty
The body is the body of an enum: variants, payloads, type parameters, doc blocks, and
public. Only the keyword is different.
The keyword is error. The parser uses the basic lexer, so error is a reserved word in
every position, and it is not an identifier. One test and four documentation pages use
error as a local name (Result.Err(error) ->); the migration renames them. err is not
used, for three reasons: Result has an .err()
method, and a reserved word cannot be a method name; err is the usual variable name for
an error; and err differs from the variant Err by case only.
2.2 The representation¶
error X: builds an EnumType with is_error = True. It is not a new type kind, so
every enum mechanism applies with no change: match and exhaustiveness, payloads, the
generic form (error DecodeError@(T)), the derived Eq, Ord, Display, hash and
clone, extension methods, perk implementations, Drop, visibility, namespaces,
libraries, the docs pass and the unused lint.
The collect pass gives the flag to seven of the nine predefined enums: StdError,
IoError, FileError, NetError, ProcessError, EnvError and MathError.
FileMode and SeekFrom are not errors and stay plain enums.
2.3 An error type is data¶
An error type can be a field, a payload, a parameter, a let, an array element and a
generic argument (List@(ConfigError), HashMap@(NetError, i32)). The flag means one
thing: "this type may be the E of a Result".
The language already uses an error as data in each of these positions. Result@(T, E)
holds E as a payload, .err() answers Maybe@(E), Result.Err(e) -> binds a local,
partition in <collections/iter> answers a list of errors, and a wrapping variant
(Unreadable(FileError)) holds one error in another. Programs collect errors, keep the
last error in a field, count errors by kind, and compare an error with an expected value
in a test.
2.4 Every Result holds an error type¶
The E of every Result@(T, E) in the program is an error type. A Result is the
value of an error channel, and only an error fits into an error channel.
- The rule covers both spellings, because
T | Eis sugar forResult@(T, E). - The rule covers every position: the signature of a function, a method, a perk
contract, a perk implementation, a lambda and a function type (
fn(i32) -> i32 | E), and also alet, a field, a payload, a parameter and a generic argument. - One code refuses every other type: a plain enum, a struct, a primitive, an array, a
function type,
MaybeandResult. CE2084 carries the rule with new wording: its meaning stays "Eis not an error type", and only the predicate changes. CE2086 is retired, and with it the list of refused names. - The rule changes at once. No release accepts both a plain enum and an error type as
an
E.
2.5 Where E3 is judged¶
- Every written
Result@(T, E)and everyT | Eis judged where it is written, through the written-type walk (validate_type_name). The HashMap key rule (reject_unusable_key) is the model. The diagnostic points at theEof a| E, and at the written type that holds theEin every other position. - A type parameter in the
Eposition (fn f@(E)() T | E, a generic type, a generic extension target, a method-level type parameter) is judged at each instance. A template is checked once, where it is written, but whetherEis an error type depends on the type argument, so E3 is part of the per-instance remainder (docs/design/checked-generics.md, rulings R7 and R8). The diagnostic names the type argument and carries a note at the template. - A
Resultthat the compiler infers is not judged. A call result comes from a signature that was judged. AResult.Err(x)construction takes its type from its position, and that position was judged.
The Result interning seam (intern_wrapper_enum) does not judge E3. It interns a type,
not a written type, so its diagnostic would have no location. It also interns the
Result types that the compiler makes (stdlib signature rows, .err(), partition,
monomorphized copies), where a refusal is an internal error. And a Result can be
interned before collect sets the flag on a type declared later in the file. The seam
may carry an internal-error backstop once (1) and (2) cover every written position.
3. Error conversion¶
3.1 The declaration¶
extend FileError as IoError:
match self:
FileError.NotFound -> return IoError.NotFound
FileError.PermissionDenied -> return IoError.PermissionDenied
FileError.Other -> return IoError.Other
extend IoError as AppError:
return AppError.Io(self)
A conversion has no name, no parameter list and no return type. The return type is the
target, and self is the source value. The wrapping case is one line.
3.2 The explicit use: as¶
e as IoError calls the declared conversion. as is the explicit conversion operator of
the language, so a conversion between two types is one more case of it. Today as casts
between numeric types only (is_valid_cast). It now also accepts a pair of error types
that has a declaration. Every other pair stays an invalid cast.
as consumes its operand by POSITION, with no marker, as a constructor argument does.
For a plain error type this is a copy, and every stdlib error type is plain. For an
error type that owns a resource, e is spent. When e is a borrow (a match binding
such as Result.Err(e) ->), the cast is the consuming use of a borrow and is refused.
The form is then e.clone() as AppError.
An identity cast x as T on a value of type T stays legal (C13; ruled 2026-10-04). It
consumes its operand by position like every as, so let string t = s as string
spends s when the type owns a resource. Before, the value had two owners.
3.3 The implicit use: ??¶
At a ?? on a Result@(T, E_in), in a body whose channel is E_out:
- If
E_inisE_out, the error propagates unchanged. - If the conversion
E_in as E_outis declared,??calls it on the error and propagates the result. - Otherwise the
??is CE2511. The help names the declaration to write.
Sushi still converts nothing on its own. A conversion runs only at a written ?? or a
written as, and only through a declaration that the program wrote.
The rule applies to every ??: in a function, a method, a lambda with a channel, and
the foreach(x?? in ...) binder, which the AST builder expands to an ordinary ??.
3.4 One step¶
FileError as IoError and IoError as AppError do not give FileError to AppError.
The lookup is an exact match on the pair. It can never be ambiguous, and the reader finds
each conversion in one place. A program that wants FileError to AppError declares it.
An identity conversion is refused. Two declarations of one pair are the duplicate-function error.
A conversion that casts a value of its own source into its own target calls itself
(C14; ruled 2026-10-04). self is always a value of the source, so self as B in the
body of extend A as B: never ends, and a cast of another A value only moves work to a
call that the body can write directly. The cast is CE2523, with a note at the
declaration. A cast of another pair in the body is legal. A recursion through another
function is not detected, as for every function.
3.5 Who may declare a conversion¶
Only the unit that declares the target type, the rule that Drop already has. For a
predefined type, the declaring unit is its home module (EnumType.home_module).
- Each pair has at most one declaration in the program, because the target's unit is
unique. Two libraries cannot both declare
FileError as AppError. - An application's
AppErrorlists the errors it absorbs, beside its own declaration. FileErrorandIoErrorboth have the home<io/error>, so the stdlib conversion lives there.- A library cannot convert its own error into
IoError, because the stdlib ownsIoError. The library declares its own error type and convertsIoErrorinto it. StdErrorhas no home module, so no unit may declare a conversion into it. A program that wants a target for its conversions declares its own error type.NetError as IoErrorlives in<io/error>, the home ofIoError. That unit then imports<net/error>, so<net/error>no longer imports<io/error>, and a program that uses<io/fs>also loads the<net/error>unit. The conversion exists because the io contracts answerIoErroronly, andTcpStreamimplements them over primitives that answerNetError(ruled 2026-10-04: keep the declared conversion, accept the import).
3.6 Which types¶
The source and the target are error types, because E3 requires it of every channel. Both
are non-generic: a generic error type, or an instance of one (DecodeError@(i32)), is
refused as a source and as a target. The lookup is then a pair of names, and the binary
manifest a pair of strings.
A generic function cannot convert an opaque error type. A template is checked once,
where it is written, with each type parameter opaque (#1070,
docs/design/checked-generics.md). A conversion is declared for a pair of named error
types, and the template cannot name one for its E. So in a template:
??on aResult@(T, E)with an opaqueEpropagates the sameEand no other (ruling R8). A body with the channel| Epropagates it unchanged.??from an opaqueEinto another error type is CE2511, with a note atE. The help names.map_err(f)??at the site, whereftakesEand answers the error type of the channel, or a concrete error type in the signature.e as AppErroron an opaqueeis CE2014, the cast rule of a type parameter.
A generic function whose error types are CONCRETE (fn f@(T)(...) T | AppError that calls
a | ParseError function) uses the declared conversion as a concrete function does.
3.7 Ownership¶
- The body consumes
self. The receiver mode isnom, and it is not written.??spends the error in any case, so a borrow would force a deep copy of the payload. - The body is bare. A
| Eon a conversion is refused, because a conversion that can fail would need a second channel at every??. A??in the body is CE0131, as in every bare body. - An error type that owns a resource (a
stringpayload) moves into the conversion. The conversion destroys what it does not put in the target.
3.8 Visibility¶
A conversion is found by its pair of types, never by a name. An import does not bring it
and does not hide it. A conversion is as visible as its target type, the rule for an
extension. A function with | AppError in its signature names AppError, so it sees
every conversion into it.
Behind an alias, the declaration spells the qualified names:
extend fs.FileError as AppError:. A private target makes the conversion usable in its
own unit only.
A conversion has no leak check. Take a public target and a private source in one unit: no other unit can hold a value of the private source, so the conversion never runs outside its unit and gives nothing away. A public function that answers the private source is a leak, and the leak check already refuses that function.
4. What ?? takes¶
?? takes a Result@(T, E), by type identity, and nothing else. CE2507 refuses every
other operand:
- A
Maybe. A??in a bare body is CE0131 as before, so??never applies to aMaybe. The CE2507 help namesor_err. The??check asks for the channel before it asks for the operand, so a bare body gets CE0131 alone. - A user enum shaped like a
Resultor aMaybe(1.4). ItsErrpayload is out of reach of E3, and itsNonehas the fault of #1168. The type identity of Sushi is nominal, so??reads the type and not its variant names.
For a Maybe, the program writes the error value:
fn first(i32[] xs) i32 | AppError:
let i32 v = xs.get(0).or_err(nom AppError.Empty)??
return Result.Ok(v)
or_err is a built-in method of Maybe@(T), beside .realise(), so it needs no import.
Its signature, written as an extension, is
extend Maybe@(T) or_err@(E)(nom self, nom E e) Result@(T, E). E is judged by E3 at
each instance.
- The parameter is
nom, because the value moves into theErrand a pass-through generic needsnom. A mode is marked at both ends, so the call writesor_err(nom AppError.Empty), asBufReader.new(nom f, 8192)does. - The receiver is
nom self, because theSomepayload moves into theOk. AMaybethat the call owns (a temporary that owns its payload,Maybe.Some(s), or a named local) is spent, which is the rule??already has for a named wrapper. - A BORROWED
Maybeis read through, as??reads through a borrowed wrapper (borrow-model.mdS10d). A get-out (xs.get(0), also from a temporary owner such asgive().get(0)), a parameter and a pattern binding are borrows. TheResultthator_erranswers then carries the borrow, solet string s = xs.get(0).or_err(nom e)??binds a borrow, exactly asxs.get(0)??did, and consuming it is the consuming-use-of-a-borrow error. No clone is needed (ruled 2026-10-04). - A read-through
or_err(a borrowedMaybewhose payload owns a resource) is legal only as the operand of??. ItsResultholds a borrowedOkand an ownedErr, and only??takes both apart: theErrmoves out, and theOkbinds a borrow. Anywhere else (alet, a method call such as.is_ok(), amatch, an argument) it is refused, and the help names.clone().or_err(...)and the??form (ruled 2026-10-04).
A None has no error value, so a conversion from "nothing" to an E is written at the
site. Otherwise the compiler would have to invent a value, which is the fault of #1168.
Rust has the same rule: ? on an Option in a function that returns a Result is a
compile error, and .ok_or(e)? is the form.
build_err_from_return_type never builds an Err from no value. A call without a value
is an internal error.
5. map_err¶
A conversion that one site needs does not need a declaration:
let File f = open(path, FileMode.Read()).map_err(|nom IoError e| AppError.Config(e))??
map_err is a built-in method of Result@(T, E), beside .realise() and .err(), so it
needs no import. Its signature, written as an extension, is
extend Result@(T, E) map_err@(F)(nom self, fn(nom E) -> F f) Result@(T, F). The
method-level type parameter F is solved from the function argument, as for the
combinators of ufcs-combinators.md.
- The function takes the error
nom, because the error moves into the new value, as it does in a conversion. A mode is part of a function type, so the lambda writes|nom IoError e|. - The receiver is
nom self, because theOkvalue and the error both move into the newResult. A namedResultis spent. A BORROWEDResultwhose payload owns a resource (a parameter, a binding, a get-out) is not read through, because the error moves intof: the call is the consuming use of a borrow (CE2411), and the help namesr.clone().map_err(f)(ruled 2026-10-04). - Two existing limits apply. A bare lambda parameter is not inferred, so the parameter
type is written. A lambda parameter cannot have an owning type, so for an error type
that owns a resource the argument is a named function
(
fn wrap(nom ParseError e) AppError).
6. Examples¶
6.1 The stdlib¶
Before:
fn open(string path, FileMode mode) File | IoError:
match fd_open(path, mode.intent(), 420):
Result.Ok(fd) -> return Result.Ok(File(fd: fd, owned: true))
Result.Err(e) -> return Result.Err(e.to_io())
After:
fn open(string path, FileMode mode) File | IoError:
let i32 fd = fd_open(path, mode.intent(), 420)??
return Result.Ok(File(fd: fd, owned: true))
6.2 A program with its own error type¶
use <io/files>
error ConfigError:
Unreadable(FileError)
Empty
extend FileError as ConfigError:
return ConfigError.Unreadable(self)
fn count_configs(string dir) i32 | ConfigError:
let string[] names = read_dir(dir)??
if (names.len() == 0):
return Result.Err(ConfigError.Empty)
return Result.Ok(names.len())
fn main() i32:
match count_configs("/etc/app"):
Result.Ok(n) -> println("{n} config files")
Result.Err(ConfigError.Unreadable(FileError.NotFound)) -> println("no such directory")
Result.Err(ConfigError.Unreadable(e)) -> println("cannot read the directory: {e}")
Result.Err(ConfigError.Empty) -> println("no config files")
return 0
6.3 Two error types in one function¶
Before:
fn notify(string host, string msg) ~ | AppError:
match connect(host, 80):
Result.Err(e) ->
return Result.Err(AppError.Net(e))
Result.Ok(nom s) ->
match s.write(msg):
Result.Err(e) ->
return Result.Err(AppError.Io(e))
Result.Ok(_) ->
return Result.Ok(~)
After:
error AppError:
Io(IoError)
Net(NetError)
Empty
extend IoError as AppError:
return AppError.Io(self)
extend NetError as AppError:
return AppError.Net(self)
fn notify(string host, string msg) ~ | AppError:
let TcpStream s = connect(host, 80)??
s.write(msg)??
return Result.Ok(~)
7. Diagnostics¶
| Fault | Diagnostic |
|---|---|
The E of a Result, in either spelling and any position, is not an error type |
CE2084, with new wording. The message says what the type is. For a plain enum, the help says to declare it with error. CE2086 is retired |
A generic instance puts a non-error type in the E position |
The same code at the instance, with a note at the template |
?? with two error types and no declaration |
CE2511. The help names extend <E_in> as <E_out>. "Not supported yet" is removed from its doc text |
e as T between two error types with no declaration |
The invalid-cast error. The help names the declaration |
e as T on a borrowed error that owns a resource |
The consuming-use-of-a-borrow error. The help names e.clone() as T |
r?? on a borrowed Result whose error type owns a resource, with or without a conversion (#1171) |
The consuming-use-of-a-borrow error. The help names r.clone()?? |
| A conversion outside the unit of its target type | CE2519, with a note at the target's declaration |
| A generic or non-error source or target | CE2520 |
| An identity conversion | CE2521 |
| A cast of the conversion's own pair in its own body (C14) | CE2523, with a note at the declaration |
| Two declarations of one pair | The duplicate-function error, with a note at the first declaration |
A \| E on a conversion |
A parse error. The grammar of 8.2 has no place for it, and no code is added |
?? on a Maybe |
CE2507, with new wording ("?? takes a Result@(T, E)"). The help names or_err(nom e) |
?? on a user enum shaped like a Result or a Maybe |
CE2507. The help says to answer a Result@(T, E) |
as with a conversion in a const initializer |
The not-a-constant-expression error (CE0108) |
A read-through or_err (a borrowed Maybe whose payload owns a resource) outside ?? (section 4) |
CE2522 |
The new codes are in internals/errors/result.py, the module that owns their range.
8. Implementation¶
8.1 The error declaration¶
- Grammar. A keyword terminal
ERRORand a top-levelerror_defwith the body ofenum_def. The parser uses the basic lexer, soERRORis a token in every position. Each segment ofuse_pathtakesNAMEorERROR, becauseuse <io/error>anduse <net/error>spell it, and the AST builder reads both tokens when it joins the path. - AST. The enum declaration node gets an
is_errorfield, set by the AST builder from the keyword. There is no second node class. collect. The flag goes on theEnumType, and on a generic template so that each instance carries it. The table of predefined enums (semantics/predefined_types.py) sets it on the seven predefined error types.- E3. One predicate, "is an error type". It is called from
validate_error_channel(passes/types/signatures.py), where CE2084 and CE2086 were emitted before this work, from the written-type walk (validate_type_name,passes/types/utils.py), and from the monomorphizer where it validates type arguments. Every call emits CE2084. CE2086 is removed frominternals/errors/types.py.signature_result_armsalso serves the backend and emits nothing. - Libraries. The
enumrow of a binary or hybrid manifest gets anis_errorfield, and so does thegeneric_typerecord of a generic enum (backend/library_format.py, read bysemantics/library_registration.py). The schema version changes, so a binary or hybrid library built before this work is refused and is rebuilt. A source.slibcarries the keyword as text.--lib-infoandslib-infoprinterror, for a generic error type too. - Display. A message that names the declaration kind says "error type" for a flagged enum.
8.2 The conversion¶
- Grammar. One more alternative of
extend_suffix:AS type ":" block. AfterEXTEND type, the tokenASselects it, asWITHandSTATICselect theirs. - AST. The node is an
ExtendDefwith the method nameas, which no user method can have becauseasis reserved. The target is in the method-level type-argument slot, soextension_symbol(source, "as", (target,))names it, and one source can convert to many targets. A conversion is not filed in the extension table, which keys on the source type and the method name. The cache signature of the unit renders the target. The body goes throughscope,typecheck,liftandborrowas any extension body, and the backend emits it as any extension. - The seam. One table,
SymbolTables.conversions, keyed(source, target)and filled bycollect. One function answers "which conversion turnsE_inintoE_out". The??check (_error_arms_agree,passes/types/expressions.py) and the cast check (is_valid_cast,passes/types/compatibility.py) both call it, and nothing else reads the table. A gate refuses a second reader. - What
??takes._unwrapped_arms(passes/types/expressions.py) asks whether the operand IS aResultinstance (is_instance_of,semantics/type_predicates.py), and no longer reads variant names. The Maybe branch is removed, and so is theerror_type=Nonepath into the backend. typecheck.validate_try_expressionandvalidate_cast_expressionask the seam and stamp the answer on the node (TryExpr.inferred_conversion,CastExpr.inferred_conversion). In a generic function the question is asked per instance.- Backend. The propagate block of
emit_try_expr(backend/expressions/try_expr.py) extracts the error, calls the stamped conversion with it as a consuming argument, runs the scope cleanup, builds theErrfrom the converted value, and returns. The conversion runs before the cleanup, and the extracted error is not registered for cleanup, so it is freed once.r??on a named wrapper (ownership.unwrap, #548) hands the error over in the same way. - Libraries. A binary or hybrid manifest gets a
conversionsrow (source, target, link symbol).--lib-infoandslib-infoprint it. effects. The destroy summary reads thepokeparameters of free functions. A conversion has none, so the hidden call at a??adds no edge.- Lints. A conversion is an
extendblock, so--warn-unusedtreats it as a root. Under--warn-missing-docs, anerrordeclaration and a conversion take a doc block, as anenumand an extension method do.
8.3 or_err and map_err¶
A built-in method had no method-level type parameter before this work. These two are the first, and
the built-in path gets four parts: the inference hook of a method family receives the
call, so a return type can depend on an argument; a solver answers the method-level type
parameter of a built-in from its arguments; a built-in carries the parameter modes and the
receiver mode for the borrow pass, so a nom marker is checked and a nom self
receiver is spent; and the backend emitter reads the stamped return type.
8.4 The IR¶
ir.md S5 moves every typecheck stamp into TypeckResults. The conversion is one more
stamp, and in SHIR one more Callee (ir.md 7.6). The error flag is a property of the
type and needs nothing from the IR.
9. Alternatives considered¶
9.1 Deferred: a from marker on a variant¶
Unreadable(FileError) from would declare extend FileError as ConfigError on the
variant, as Rust's thiserror does with #[from]. The one-line body of 3.1 does the
same job, so the marker waits for a real need. When it comes, it is legal only on a
variant of an error declaration with one payload of a non-generic error type, and it
follows every rule of section 3.
9.2 Other ways to mark an error type¶
| Form | Prior art | Why not |
|---|---|---|
An Error perk |
Swift : Error, Go Error() string |
Explicit, it puts extend X with Error: on every error type. Derived for every enum, it marks nothing |
| A separate type kind | Zig error{...}, OCaml exception |
Every enum seam would need a second handler. The flag gives the same language |
| No mark | Rust | It keeps the problem of 1.1 |
9.3 Other ways to apply E3¶
- Channels only.
let Result@(i32, Color) rwould stay legal: aResultthat no channel can propagate. - At the interning seam. No location, compiler-made
Resulttypes, and the order ofcollect(2.5).
9.4 Other ways to convert¶
| Form | Prior art | Why not |
|---|---|---|
A predefined perk From@(E) |
Rust From, applied by ? |
It needs a perk with a type parameter (CE4010), a static in a perk (CE4014), and more than one implementation of one perk on one type: three language changes |
| Structural error unions | Zig error sets, Roc tag unions, OCaml polymorphic variants | Type identity is nominal (type-identity.md), and Zig errors carry no payload |
| One dynamic error type | Rust anyhow, Go error, Swift any Error, exceptions |
Sushi has no dynamic perk object, and a top type loses the exhaustive match |
| Explicit mapping only | Go, Gleam, Swift typed throws, C++ std::expected |
It is the cost of 1.2. map_err (section 5) keeps it for a single site |
A binary r ?? conv |
none | In C#, JS, Swift, PHP and Kotlin a binary ?? or ?: means "use this default" |
| A transitive chain | none | The lookup becomes a path search, and two paths can give two answers |
Conversion at an assignment, an argument or a return |
C++ implicit conversions | No written mark shows it. Only ?? and as mark the site |
| The unit of either type may declare | Rust's orphan rule | Two units can declare one pair, and the clash must be found across units and libraries |
9.5 ?? on a Maybe for StdError only¶
?? would stay legal on a Maybe where E is StdError, and build StdError.Error.
Fewer sites change, but the compiler still supplies a value that the source does not
spell.
9.6 Other choices on the details¶
| Choice | Why not |
|---|---|
| A new code for E3, and CE2084 and CE2086 both retired | CE2084 already means "E is not an error type". Only its predicate changes, and its fixtures keep their code |
as with a nom marker (nom e as T) |
?? spends the error with no marker, and a constructor consumes its argument by position. as is one more position of that kind |
as that borrows its operand |
One body cannot have two receiver modes. The conversion consumes self for ??, so as consumes too |
| A leak check on a conversion | A conversion with a private source never runs outside its unit, so the check would refuse nothing that matters |
?? that keeps reading variant names |
It leaves a path around E3 and around the fix of #1168, and it reads the shape of a type, where identity is nominal |