The Error trait
Error is a trait, not a base type; a failure that implements it fits into Result<Value, Error> for the layers that only need to report it, and cause() gives the chain.
Not built natively yet. A ? that converts the error on the way out is not built by the native back end yet, so
torb run refuses the examples here that use it. torb check accepts them, and the rules are the language's.
Every error type in this documentation so far has been precise: ConfigError, IoError, a type named for the one
thing that can go wrong. Error exists for the layers above those, where the only thing left to do with a failure is
hand it further up and eventually report it.
Example#
type ConfigError with Show, Error {
case Missing(key: String)
fn show(): String {
match self {
.Missing(key) => "missing: {key}"
}
}
}
fn readPort(): Result<Int, ConfigError> {
Fail ConfigError.Missing("port")
}
fn start(): Result<Void, Error> {
const port = readPort()?
print "Listening on {port}"
Ok(void)
}
print start()Syntax#
public trait Error with Show {
fn cause(): Error? { None }
}
type <Name> with Show, Error {
...
}Rules#
Erroris a trait, not a base type. There is no class every error extends; a type becomes one by writingwith Errorand implementingShow, the one traitErrorrequires.A concrete failure becomes an
Errorvalue through the ordinary coercion of a value to a trait it implements.?needs no special rule forResult<Value, Error>: aConfigErrorthat carriesErrorconverts to it the same way aCircleconverts toShapewhereverShapeis expected.cause()is the chain. An error that wraps another one returns it fromcause(), so a report can walk the chain link by link. The end of the chain, and any error that wraps nothing, answersNone.The default
cause()answersNone, so a type with nothing to add writeswith Errorand nothing else. Only a type that wraps another error overrides it.type ConfigError with Show, Error { case Missing(key: String) fn show(): String { match self { .Missing(key) => "missing: {key}" } } } print ConfigError.Missing("port").cause()Precise error types stay the norm for a library. A caller can only
matchon what a signature names, so a function that answersResult<Config, Error>instead ofResult<Config, ConfigError>takes away its caller's ability to handle one failure differently from another.Erroris forfn main(): Result<Void, Error>and the layers in between, not for a library's own public functions.
What this is not#
Error is not Exception, and a value that carries it is not thrown. There is no throw, no catch, and no
stack unwinding anywhere in the language; a value with Error travels exactly the way any other value does, inside a
Result, returned and matched on like anything else.
type ConfigError with Show, Error {
case Missing(key: String)
fn show(): String {
match self {
.Missing(key) => "missing: {key}"
}
}
}
fn report(error: Error) {
print "error: {error}"
if const Some(cause) = error.cause() {
print " caused by: {cause}"
}
}
report ConfigError.Missing("port")type ConfigError with Show, Error {
case Missing(key: String)
fn show(): String {
match self {
.Missing(key) => "missing: {key}"
}
}
}
fn start(): Result<Void, ConfigError> {
try {
Fail ConfigError.Missing("port")
}
}
print start()
// error: Cannot find `try` herecause() is not a stack trace. It is one link the error itself chose to keep, not a record of where the failure
happened. In the debug profile, ? records where a failure was handed on separately from the error value; see
Errors at the top level.
Implementing Error is not implementing Show for free, or the reverse. Error requires Show, so a type needs
both written down (or Show generated, where the type qualifies); with Error alone, without Show, does not
compile.
Related#
- Result - where a failure lives before it becomes an
Errorvalue. - Declaring an error type - building the precise error types
Errorsits above. - std/core - the trait's declaration in full.