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.
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
.
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.
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.
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
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.
Add or_error
to your Gleam project.
gleam add or_error
This library is inspired by Ocaml's Or_error
module.