type-safe-errors
provides type-safe domain errors to Typescript.
It comes with an async promise-like interface but with strong-typed handleable errors.
npm i type-safe-errors
import { Ok, Err } from 'type-safe-errors';
class InvalidCredentialsError extends Error {
name = 'InvalidCredentialsError' as const;
}
function authorizeUser(username: string, password: string) {
if (username === 'admin' && password == 'admin') {
return Ok.of({
name: 'admin',
isAdmin: true,
});
} else {
return Err.of(new InvalidCredentialsError());
}
}
authorizeUser('admin', 'admin')
.map((user) => {
console.log('authorized! hello ', user.name);
})
.mapErr(InvalidCredentialsError, (err) => {
// err is fully typed err object (InvalidCredentialsError class instance)
console.log('Invalid credentials!', err);
});
It's common case to start results chain from an async call, e.g. call to your database or external API. It can be handle in few ways, but simplest is to use dedicated helper function: Result.from.
import { Ok, Err, Result } from 'type-safe-errors';
class InvalidCredentialsError extends Error {
name = 'InvalidCredentialsError' as const;
}
async function authorizeUser(username: string, password: string) {
// simulate async call
const storedPassword = await Promise.resolve('admin');
if (username === 'admin' && password === storedPassword) {
return Ok.of({
name: 'admin',
isAdmin: true,
});
} else {
return Err.of(new InvalidCredentialsError());
}
}
Result.from(() => authorizeUser('admin', 'admin'))
.map((user) => {
console.log('authorized! hello ', user.name);
})
.mapErr(InvalidCredentialsError, (err) => {
// err is fully typed err object (InvalidCredentialsError class instance)
console.log('Invalid credentials!', err);
});
If you work with rich business logic it's common to use exceptions in js to represent different states of the application. The problem with this solution and TypeScript is that when you catching an exception, you lost information about it's types.
Let consider this simplified example from an express controller:
try {
await payForProduct(userCard, product);
} catch (err) {
res.send(500, "Internal server error");
}
By looking at this code, you can not determine what kind of exception can happen.
Of course, you can check payForProduct
body, but what if it's called other functions? And they call more? For rich business logic, it's unmaintainable to follow all
possible custom exception just by reading the code.
Because of this, it's common to just return 500
in such cases (express
doing it by default). But there can be many errors that should be handled differently than 500
status code. For example, maybe the user does not set any address data yet? Maybe his cart expired? Or did he provide an invalid CVC number?
The client app should be informed of the reason, for example, by 400
status code and error details in the response body. But first, to properly handle the errors, the developer must be aware of what errors can happen.
This is the problem that type-safe-errors
library is trying to solve.
(Full example: ./examples/basic-example)
Learning and using type-safe-errors
should be simple and straightforward. To achieve this, the API must be as simple and as intuitive as possible. It's one of the reasons why the result class is always async (e.g. neverthrow have two different result types, one for sync and one for async results handling).
The long-term goal is not to handle every possible use case. Instead, it's to do one thing well - providing a way to handle domain exceptions in a strong-typed, future-proof way.
Using type-safe-erros
should be similar in feel to work with traditional js promises. You can map any success result (same like you can then fulfilled promise) or mapAnyErr (same as you can catch rejected promise).
You could notice that the type-safe-error
project is somehow based on Either concept from functional programming. But the goal was not to follow the idea closely but to provide an easy-to-use API in practical js work, focused on async programming.
- Expressive error handling in TypeScript and benefits for domain-driven design
- STOP throwing Exceptions! Start being Explicit
- 200 OK! Error Handling in GraphQL
- neverthrow
- Khalil Stemmler: Flexible Error Handling w/ the Result Class
- Functional Error Handling with Express.js and DDD
- Either
Quick fix: upgrade your tsconfig.json
file compilerOptions.target
option to at least es6
.
The type-safe-errors
library depends on instanceof
standard JS feature.
But extending by JavaScript Error does not works correctly for TypeScript compilation target es5
and below. This is explained on TypeScript wiki.
One option is to upgrade tsconfig.json
file compilerOptions.target
to es6
or higher version.
An alternative is to abstain from extending by JavaScript Error class, e.g.
// original
class InvalidCredentialsError extends Error {
name = 'InvalidCredentials' as const;
}
// without Error parent
class InvalidCredentialsError {
name = 'InvalidCredentials' as const;
}
This works for most cases, but be aware that sometimes it can give a rejection of a non-Error JavaScript object (instance of your class). It can interfere with some other tools (e.g. Mocha can sometimes show some cryptic error messages when a test fails).