Result
APIError handling in a type safe way
Motivation
A Result<Value, Error>
is a type to manipulate and propagate errors in a type safe way (like Rust result or Haskell Either).
It can be represented as a tagged union { _: 'Ok', value: Value } | { _: 'Error', error: Error }
.
Historically, Javascript programs tends to throw errors. But this practice have the following drawbacks :
- Runtime crash (ex: accessing a property of
undefined
) and custom business errors (ex: the API returned an error) are mixed and are both caught with try/catch. - Caught errors have always an
unknown
type, they are hard to handle in a type safe way - Developers are not aware that they should handle some error cases, as it is not represented in the type system (ex: division by zero)
On the contrary, Result
have the following capabilities :
- Developer must handle
Ok
/Error
case to be able to use thevalue
orerror
- All error cases can be represented using a union type
Result<V, E1|E2|E3>
- Runtime crashes follow a complete different path, and therefore can be handled in a proper way (stop the program, log in a crash reporter, etc)
Usage
An enum can be declared as the following example :
import { Result } from '@w5s/core';
import { CustomError } from '@w5s/error';
export interface ZeroDivisionError
extends CustomError<{
name: 'ZeroDivisionError';
}> {}
export const ZeroDivisionError = CustomError.define<ZeroDivisionError>({ errorName: 'ZeroDivisionError' });
export function divide(value: number, divider: number): Result<number, ZeroDivisionError> {
const returnValue = value / divider;
return Number.isNaN(returnValue) ? Result.Error(ZeroDivisionError()) : Result.Ok(returnValue);
}
Matching values
Method 1: Result.isOk
/ Result.isError
(Recommended)
- ✓ Good performance
- ✓ Long term maintainable
const program = (result: Result<number, 'FooError'>) => {
if (Result.isOk(result)) {
console.log(result.value);
} else {
console.error(result.error);
}
}
Method 2: if(result.ok)
- ✓ Highest performance
- ✓ No module load overhead
- ⚠️ Potentially less maintainable on long term
const program = (result: Result<number, 'FooError'>) => {
if (result.ok) {
console.log(result.value);
} else {
console.error(result.error);
}
}
Chaining
Use Result.map
and/or Result.andThen
to transform Ok
value
Using pipeline operator (Draft proposal)
function program(expression: string) {
// Convert string to number
return parseNumber(expression) // Result<number, ParseError>
// Divide by 2
|> Result.andThen(#, (_) => divide(10, 2)) // Result<number, ZeroDivisionError | ParseError>
// Multiple by 3
|> Result.map(#, (_) => _ * 3); // Result<number, ZeroDivisionError | ParseError>
}
Using const
function program(expression: string) {
// Convert string to number
const parsed = parseNumber(expression); // Result<number, ParseError>
// Divide by 2
const dividedBy2 = divide(parsed, 2); // Result<number, ZeroDivisionError | ParseError>
// Multiple by 3
const multipliedBy3 = Result.map(dividedBy2, (_) => _ * 3); // Result<number, ZeroDivisionError | ParseError>
return multipliedBy3;
}
Handling error
Use Result.mapError
and/or Result.orElse
to transform Error
error.
const handleZeroDivisionError = <E>(result: Result<number, E|ZeroDivisionError>) => {
return Result.orElse(result, (error) => {
switch (error.name): {
case ZeroDivisionError.typeName: return Result.Ok(0);
default: Result.Error(error);
}
});// Result<number, E>
};
Coding Guide
FAQ
Why choose the name Result
over Either
?
Result
over Either
?It is a matter of preference. Ok
/ Error
is more explicit than Left
/ Right
.
Generally speaking, W5S
packages naming tends to be often aligned with the Rust
naming when no ECMA equivalent exists.