or_error
An OrError
is an ad-hoc error type. Use OrError(value)
in functions that may fail.
An OrError
can be quickly constructed from any type of Result
. This makes combining error types from different libraries much easier. However it comes at the cost of being less typeful: you can’t recover the original error value.
Because an OrError
is just a Result
with a fixed error type, it forms a monad with a single map function, which is useful for effortlessly handling errors in your codebase.
Further documentation can be found at https://hexdocs.pm/or_error.
Comparison of error handling
or_error
excels at handling errors of different types.
For a simple comparison, let’s try to read an Int
from a json file using simplifile
and gleam/dynamic/decode
.
Method 1: Nested Result
s
fn read_int(file_path: String) -> Result(Result(Int, json.DecodeError), simplifile.FileError) {
use content <- result.map(file_path |> simplifile.read())
content |> json.parse(decode.int)
}
This method works, but the return type is unweildy at best. Pattern matching on it in future is cumbersome.
Method 2: A custom error type
type FileParseError {
FileReadError(simplifile.FileError)
JsonParseError(json.DecodeError)
}
fn read_int(file_path: String) -> Result(Int, FileParseError) {
use content <- result.try(
file_path
|> simplifile.read()
|> result.map_error(fn(read_error) { FileReadError(read_error) }),
)
content
|> json.parse(decode.int)
|> result.map_error(fn(parse_error) { JsonParseError(parse_error) })
}
This flattening of the error types provides a much nicer interface for callers to work with. However it takes longer to write and eventually results in huge, nested error variant types.
Method 3: With or_error
If we don’t care to track the error typefully, we can take a pragmatic approach by converting relevant error information to a human-readable string.
fn read_int(file_path: String) -> OrError(Int) {
use content <-
file_path
|> simplifile.read()
|> or_error.of_result("Failed to read file" <> file_path)
|> or_error.bind_() // `bind_()` is a pipe-able version of `bind()`
content
|> json.parse(decode.int)
|> or_error.of_result("Failed to parse Int from content")
}
To each or_error.of_result()
call, we pass some human-readable context. The error itself will also be recorded; it is constructed by string.inspect(error)
.
This method scales well as codebases become more complex, and error types more numerable. However, we have lost type information about the errors themselves.
If we were to call or_error.unwrap_panic()
with this function on a nonexistent file, we would see:
runtime error: panic
error: Enoent
context: Failed to read file does_not_exist.json
An (almost drop-in) replacement for gleam/result
or_error
is intended as a replacement for gleam/result
.
As such, all relevant functions are reimplemented, with a few exceptions:
try_recover
: This is omitted since recovering from a string alone is itself error-prone.try
: This is renamed tobind
in reference to the type’s monadic nature.map_error
: Since there is only one error type, which is morally a string, this function does not make sense.unwrap_error
,unwrap_both
andreplace_error
: Since the error type is not directly usable, there is no purpose to these functions.
Additionally, some other functions are provided, mostly to make code more pipe-friendly:
bind_
andmap_
: Pipe-able versions ofbind
andmap
.return
: Equivalent to constructingOk
, named in reference to the type’s monadic nature.fail
: Allows direct construction of theError
case.unwrap_panic
: Panic if theOrError
is notOk
. A pipe-able alternative forlet assert ...
.pretty_print
: A self-explanatory function, also used byunwrap_panic
.of_result
: A quick way to construct anOrError
, usingstring.inspect
on any error.
Installation
Add or_error
to your Gleam project.
gleam add or_error
Prior art
This library is inspired by Ocaml’s Or_error
module.