This library provides a Result type for Typescript, allowing for better and safer error handling.
- Rust-like Result type
- Better error handling
- Automatic type inference
- More robust code
- Zero dependencies
- Minimalistic
- Small package size
Imagine having a function which you use to split time into seconds and minutes. We will look at one implementation which uses result and a second one which does not.
// returns {hours: number, mins: number}
function parseTime(time: string) {
const splitTime = time.split(":")
return {
hours: parseInt(splitTime[0], 10),
mins: parseInt(splitTime[1], 10),
}
}
Now you call parseTime
in a different place of our codebase.
This function uses a .split
and relies on the result being at least 2 items long.
Because of that, you have to keep in mind that this function could throw, even though there is no indication by the type system that it could be the case.
This leads to uncaught errors.
Somehow, the function get called with an incorrect argument, for example "2051" instead of "20:51".
This arugment is however still a string which makes typescript unable to help us catch this error.
function faultyArgument() {
const time = "2051"
const result = splitTime(time)
// You do not have any indication by the type system that this could throw.
// You forget to use a try catch segment and end up with a runtime error
return result
}
This is when the Result
class comes in. Result indicates a computation which could fail. At runtime, could be either an Ok
or an Err
depending on cirumstances.
The massive benefit we get with Result
is that we do not catch errors like the previously mentioned one at runtime , but rather at compilation time .
Let's look at the previous example with Result
function parseTime(time: string) {
const splitTime = time.split(":")
if (splitTime.length !== 2) {
return Result.err("SPLIT_ERROR")
}
if (isNaN(parseInt(splitTime[0], 10)) || isNaN(parseInt(splitTime[1], 10))) {
return Result.err("PARSE_ERROR")
}
if (parseInt(splitTime[0], 10) > 23 || parseInt(splitTime[1], 10) > 59) {
return Result.err("VALUE_ERROR")
}
return Result.ok({
hours: parseInt(splitTime[0], 10),
mins: parseInt(splitTime[1], 10),
})
}
Now, using the Result pattern, we are forced to deal with the fact that it could fail at compilation time .
Better yet, we know exactly which errors can occur and we can handle them accordingly.
For example:
function faultyArgument() {
const time = "2051"
const result = parseTime(time)
// type is Result<{hours: number, mins: number}, "SPLIT_ERROR" | "PARSE_ERROR" | "VALUE_ERROR">
// Here you gracefully handle the error case
if (result.isErr()) {
// errValue is only available after the type system is sure that the result is an Err
switch (result.errValue) {
case "SPLIT_ERROR":
console.log("The time was not in the correct format")
break
case "PARSE_ERROR":
console.log("The time contained non-numeric characters")
break
case "VALUE_ERROR":
console.log("The time contained invalid values")
break
}
return
}
// Here the type system is sure that the result is an Ok, and we get access to the "value" property
const { hours, mins } = result.value
console.log(`The time is ${hours}:${mins}`)
}
As you can see, it is much harder to shoot yourself in the foot while handling errors, making our code much more robust.
Whenever possible, the result return type gets inferred automatically for the best dev experience possible.
A class representing a computation which may succeed or fail.
A class representing a successful computation.
A class representing a failed computation.
Creates a new Ok
variant; If no value is provided, it defaults to null
.
static ok<T>(value?: T): Ok<T>
Creates a new Err
variant. If no value is provided, it defaults to null
. Optionally takes an origin
argument which is the original error that was thrown.
static err<E>(errValue?: E, origin?: Error): Err<E>
static from<T, E>(fnOrThenable: (() => T | Promise<T>) | Promise<T>, errValue?: E): Promise<Result<T>>
Creates a Result
from a function, a promise, or a promise-returning function.
If an error is thrown at any point, it is caught and wrapped in an Err
. Takes an optional errValue
argument which will be the value contained in the Err
variant. The origin
property of the Err
will be the original error that was thrown.
If the function or promise resolves successfully, the value will be wrapped in an Ok
.
Returns true
if the result is an Ok
variant. If true, casts the result as Ok
isOk(): this is Ok<T>
Returns true
if the result is an Err
variant. If true, casts the result as Err
isErr(): this is Err<E>
Returns the contained Ok
value. Throws an error if the value is an Err
.
unwrap(): T
Returns the contained Err
value. Throws an error if the value is an Ok
.
unwrapErr(): E
Returns the contained Ok
value. If the value is an Err
, returns the provided default value.
unwrapOr(defaultValue: T): T
Returns the contained Ok
value. If the value is an Err
, throws an error with the provided message.
expect(message: string): T
Returns the contained Err
value. If the value is an Ok
, throws an error with the provided message.
expectErr(message: string): E
Calls the appropriate function based on the result based on if it is an Ok
or an Err
.
match<U>(fn: { ok: (value: T) => U; err: (errValue: E) => U }): U
Calls the provided function if the result is an Ok
. If the result is an Err
, returns the Err
value.
andThen<U>(fn: (value: T) => Result<U, E>): Result<U, E>
Maps a Result<T, E>
to Result<U, E>
by applying a function to a contained Ok
value, leaving an Err
value untouched.
map<U>(fn: (value: T) => U): Result<U, E>
Maps a Result<T, E>
to Result<T, F>
by applying a function to a contained Err
value, leaving an Ok
value untouched.
mapErr<F>(fn: (errValue: E) => F): Result<T, F>
Wraps a function that returns a Result
but may still throw an error, in which case it is caught and wrapped in
an Err
with the provided error value.
A good example of when you would use this is when communicating with a database. You can have a generic error for when the communication fails, but you can also have more specific errors for constraint violations, etc.
Does not yet work with type unions as T because of weird ts behavior
static tryCatch<T, E>(fn: () => Result<T,E> | Promise<Result<T,E>>, errValue?: E): Result<T, E>
Sometimes type inference does not work well with Result
unions. You might notice that your arguments are being inferred as any
or that the return types are not correct.
This can be the case when using andThen , map , mapErr , or match .
When this happens, call this function to get a type that is easier to work with.
static infer<T extends Result>(result: T): T
The value contained in the Ok
variant.
value: T
The value contained in the Err
variant.
errValue: E
The original error that was thrown.
origin: Error
Logs the error to the console.
log(): this