Define an error type
Declare a type with one case per distinct failure, add Show and Error where a layer above needs to hand it further up, and let the generated From do the conversion at every ?.
Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite
An error type is an ordinary type with cases, and the only design decision is which case wraps what: a case that
wraps one field of a type no sibling case wraps gets a From generated for it, which is what lets ? convert between
error types without a line of code.
Steps#
Declare a case per distinct source of failure, not per call site. A case can wrap a nested error, or carry a
Stringfor a failure that has no type of its own to wrap.type ConfigError { case Missing(key: String) case Invalid(key: String, reason: String) }Wrap a lower-level error in its own case so
?converts it for free. A case that wraps exactly one field, of a type no other case of the same error type wraps, getsFromgenerated.readPort()?inside a function that answersAppErrorneeds nothing else onceAppErrorhas a case that wrapsConfigErrorand no other case does.type AppError { case Config(cause: ConfigError) case Startup(message: String) }Add
with Show, Erroron the type that a layer above hands further up as "some error".Showis the one memberErrorrequires and the one you write;Error's owncause()already has a default.type ConfigError with Show, Error { case Missing(key: String) case Invalid(key: String, reason: String) fn show(): String { match self { .Missing(key) => "{key} is missing" .Invalid(key, reason) => "{key} is invalid: {reason}" } } }Override
cause()only on a case that wraps another error. The default answersNone, which is correct for every case that carries nothing but its own data.fn cause(): Error? { match self { .Config(cause) => Some cause .Startup(_) => None } }Answer the precise type from a function that has one caller who needs to tell failures apart, and reserve
Result<Value, Error>forfn mainand the layers that only report. A caller can onlymatchon what the signature names, so widening too early takes that ability away.
Pitfalls#
- Two cases that wrap the same type stop
Fromfrom being generated for either one. The conversion would be ambiguous, so the checker rejects it and a wrapper has to be built by naming the case explicitly (Fail AppError.Config(cause)). - A
Stringfield is not a distinct type.case Startup(message: String)never gets a generatedFrom, because the checker cannot tell whichString-carrying case a plainStringbelongs to. Build it by name:Fail AppError.Startup("nothing to do"). with Erroralone does not compile.ErrorrequiresShow, so both have to be written (orShowleft to the generated one, where the type has no cases and every field supports it).- Nesting every failure into one application-wide type is not free. A case per distinct failure source keeps every
?conversion generated; a case that duplicates a type another case already wraps costs an explicit conversion at every site that raises it.
Full example#
type ConfigError with Show, Error {
case Missing(key: String)
case Invalid(key: String, reason: String)
fn show(): String {
match self {
.Missing(key) => "{key} is missing"
.Invalid(key, reason) => "{key} is invalid: {reason}"
}
}
}
type AppError with Show, Error {
case Config(cause: ConfigError)
case Startup(message: String)
fn show(): String {
match self {
.Config(cause) => "configuration problem: {cause}"
.Startup(message) => "could not start: {message}"
}
}
fn cause(): Error? {
match self {
.Config(cause) => cause
.Startup(_) => None
}
}
}
fn readPort(): Result<Int, ConfigError> {
Fail ConfigError.Missing("port")
}
fn start(): Result<Void, AppError> {
const port = readPort()? // ConfigError -> AppError through the generated From
print "Listening on {port}"
Ok(void)
}
fn report(error: AppError) {
print "error: {error}"
if const Some(cause) = error.cause() {
print " caused by: {cause}"
}
}
match start() {
Ok(_) => {}
Fail(problem) => report problem
}Related#
- Declaring an error type - the generated
Fromand the case rules in full. - The Error trait -
cause(), and whenResult<Value, Error>is the right signature. - The question mark operator - what the generated
Frombuys at every?. - Read a file - the same pattern applied to
IoError.