type-safe-errors
TypeScript icon, indicating that this package has built-in type declarations

0.5.1 • Public • Published

Type-safe errors in TypeScript

Overview

type-safe-errors provides custom type-safe errors to Typescript.

The library offers an async promise-like interface and ensures type safety with easy-to-handle errors.

Motivation

The type-safe-errors library was made to solve these problems:

  • In TypeScript, when promises are rejected, they lose their error types. It's tough to keep these error types correct, and it gets even harder over time.
  • Not all error in the code should lead to throws. Sometimes, it makes more sense to think of an error as just the result of some logic. For example, if there's a problem connecting to a database, that's a good time to use a throw. But an invalid password should be seen as a result of validation logic, not an exception

Table Of Contents

Installation

npm i type-safe-errors

Basic example

In this example, we demonstrate how to use the type-safe-errors library to handle user authorization with a simple username and password check.

import { Ok, Err } from 'type-safe-errors';

class InvalidCredentialsError extends Error {
  name = 'InvalidCredentialsError' as const;
}

// Function to authorize an user based on their username and password
function authorizeUser(username: string, password: string) {
  if (username === 'admin' && password === 'admin') {
    // Return an Ok result with the user information
    return Ok.of({
      name: 'admin',
      isAdmin: true,
    });
  } else {
    // Return an Err result with an InvalidCredentialsError instance
    return Err.of(new InvalidCredentialsError());
  }
}

authorizeUser('admin', 'admin')
  // Handle successful authorization
  .map((user) => {
    console.log('authorized! hello ', user.name);
  })
  // Handle error in case of invalid credentials
  .mapErr(InvalidCredentialsError, (err) => {
    // here `err` is fully typed object of InvalidCredentialsError class
    console.log('Invalid credentials!', err);
  })
  // Map the result to classic promise
  // This is optional, but highly recommended, as it allows TypeScript to detect 
  // not handled errors, in current code and in the future
  .promise();

Async basic example

It's common to start a result chain with an async call, such as a call to your database or an external API.

There are a few ways to handle this, but the simplest is to use the dedicated helper function: Result.from.

import { Ok, Err, Result } from 'type-safe-errors';

class InvalidCredentialsError extends Error {
  name = 'InvalidCredentialsError' as const;
}

class UserNotFoundError extends Error {
  name = 'UserNotFoundError' as const;
}

async function authorizeUser(username: string, password: string) {
  if (username !== 'admin') {
    return Err.of(new UserNotFoundError());
  }

  // simulate async call
  const storedPassword = await Promise.resolve('admin');
  if (password !== storedPassword) {
    return Err.of(new InvalidCredentialsError());
  }

  return Ok.of({
    name: 'admin',
    isAdmin: true,
  });
}

Result.from(() => authorizeUser('admin', 'admin'))
  .map((user) => {
    // here `user` type is { name: string, isAdmin: boolean }, from `authorizeUser` return type
    console.log('authorized! hello ', user.name);
  })
  .mapErr(UserNotFoundError, (err) => {
    // here `err` is fully typed object of InvalidCredentialsError class
    console.log('Invalid user name!', err);
  })
  .mapErr(InvalidCredentialsError, (err) => {
    // here `err` is fully typed object of InvalidCredentialsError class
    console.log('Invalid credentials!', err);
  });

Description

If you work with rich business logic, it's common to use exceptions to represent different states of the application. The problem with this solution and TypeScript is that when you catch an exception, you lose information about its 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 cannot determine what kind of exception can happen. Of course, you can check the payForProduct body, but what if it's called by other functions, which in turn call additional functions?
When dealing with advanced business logic, it becomes difficult to maintain an understanding of all possible scenarios that can lead to errors solely by reading the code.

That's why it's common to just return 500 in such cases (express does it by default). However, there can be many errors that should be handled differently than a 500 status code. For example:

  • Maybe the user did not set any address data yet?
  • Maybe the user's cart has expired?
  • Maybe the user provided an invalid CVC number?

In each of these cases, the server should return a different status code along with specific error details. The client app should be informed of the reason, for example, by a 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.

screen-gif

(Full example: ./examples/basic-example)

Philosophy

Minimal API

Keeping the API minimal reduces the learning curve and makes it easier for developers to understand and use the library effectively.

It's one of the reasons why the result class is always async (for example, neverthrow has two different result types, one for synchronous and one for asynchronous 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.

Practical API

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 may 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 TypeScript work, focused on async programming.

Inspiration

Troubleshooting

Errors seem not to be caught by .mapErr(...) function

Quick fix: Update your tsconfig.json file compilerOptions.target option to at least es6.

The type-safe-errors library depends on instanceof standard JS feature. The instanceof feature allows checking if an object is an instance of a particular class or constructor. In the context of type-safe-errors, it is used to verify if an error object is an instance of a specific error class.

However, extending the JavaScript Error does not work correctly for TypeScript compilation targets es5 and below. This issue is explained on TypeScript wiki.

One option is to update tsconfig.json file compilerOptions.target to es6 or a 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 result in a rejection of a non-Error JavaScript object (instance of your class). This may interfere with some other tools (for example, Mocha can sometimes show cryptic error messages when a test fails).

Package Sidebar

Install

npm i type-safe-errors

Weekly Downloads

1,084

Version

0.5.1

License

MIT

Unpacked Size

143 kB

Total Files

42

Last publish

Collaborators

  • psychowico