Skip to content

Repository files navigation

error-kid

NPM Size code-badge

A simple toolkit to work with custom errors. Definitely not a kid.

Why

Declaring a custom error class in TypeScript involves more boilerplate than it should:

class TimeoutError extends Error {
  constructor(public readonly duration: number) {
    super(`Timed out: ${duration}ms`);
    // Not inherited from the class, must be assigned manually.
    this.name = 'TimeoutError';
    // Required to keep `instanceof` working when targeting ES5.
    // Easy to forget, and silently breaks error handling when omitted.
    Object.setPrototypeOf(this, TimeoutError.prototype);
  }
}

const err = new TimeoutError(1000);
if (err instanceof TimeoutError) {
  // err is TimeoutError
}

error-kid does all of it for you, and adds a typed is predicate along the way:

import { errorClassWithData } from 'error-kid';

class TimeoutError extends errorClassWithData<
  { duration: number }, [duration: number]
>({
  name: 'TimeoutError',
  data: duration => ({ duration }),
  message: duration => `Timed out: ${duration}ms`,
}) {}

const err = new TimeoutError(1000);
if (TimeoutError.is(err)) {
  // err is TimeoutError
}

Installation

# yarn
yarn add error-kid

# pnpm
pnpm i error-kid

# npm
npm i error-kid

errorClass

A function used to create a new error class without custom data.

import { errorClass } from 'error-kid';

class UnknownError extends errorClass({ name: 'UnknownError' }) {}
UnknownError.name; // 'UnknownError'

const error = new UnknownError();
error.message; // ''
error.cause; // undefined
error instanceof Error; // true

UnknownError.is(new Error); // false
UnknownError.is(error); // true

By default, created error class constructor accepts no arguments. It also passes nothing to the Error super constructor.

Note

Note that all examples in this document use the class Err extends errorClass(...) {} form instead of const Err = errorClass(...). This is intentional. The function returns a value, so assigning it to a variable makes Err usable as a value only — using it as a type will not work. Declaring a class, in turn, creates both a value and a type with the same name:

const ConstError = errorClass({ name: 'ConstError' });
// Error: 'ConstError' refers to a value, but is being used as a type.
function handle(error: ConstError) {}

class ClassError extends errorClass({ name: 'ClassError' }) {}
// Works as expected.
function handle(error: ClassError) {}

message

To specify the error message, use the message option. It accepts either a static string, or a function computing the message from the constructor arguments.

import { errorClass } from 'error-kid';

const TimeoutError = errorClass({ name: 'TimeoutError', message: 'Timed out' });
new TimeoutError().message; // 'Timed out'

To compute the message dynamically, define the constructor arguments' type using the generic parameter. It must be any tuple, and it describes arguments passed to the error class constructor.

import { errorClass } from 'error-kid';

class ApiError extends errorClass<[
  errorText: string,
  retriesCount: number,
]>({
  name: 'ApiError',
  message: (errorText, retriesCount) => {
    return `Request failed. Retries count: ${retriesCount}. Error text: ${errorText}`;
  },
}) {}

const error = new ApiError('Ooopsie!', 3);
error.message; // "Request failed. Retries count: 3. Error text: Ooopsie!"

cause

The cause option is a function computing the error cause from the constructor arguments. Its returned value is passed to the Error super constructor as the cause property of ErrorOptions.

Most commonly the cause is one of the constructor arguments:

import { errorClass } from 'error-kid';

class ApiError extends errorClass<[status: number, cause?: unknown]>({
  name: 'ApiError',
  message: status => `Request failed with status ${status}`,
  cause: (_status, cause) => cause,
}) {}

const error = new ApiError(500, new Error('ECONNRESET'));
error.message; // 'Request failed with status 500'
error.cause; // Error('ECONNRESET')

To use a constant cause, return it from a function accepting no arguments:

import { errorClass } from 'error-kid';

class DatabaseError extends errorClass({
  name: 'DatabaseError',
  message: 'Failed to connect',
  cause: () => new Error('ECONNREFUSED'),
}) {}

new DatabaseError().cause; // Error('ECONNREFUSED')

When the option is omitted, cause is undefined.

is

Each created class has a static is method — a type predicate checking if the passed value is an instance of this class. Being a standalone function, it can be passed anywhere a predicate is expected.

import { errorClass } from 'error-kid';

class TimeoutError extends errorClass({ name: 'TimeoutError' }) {}

try {
  // ...
} catch (error) {
  if (TimeoutError.is(error)) {
    // `error` is narrowed to TimeoutError here.
    error.message;
  }
}

// Narrowing works in predicate positions too.
const timeouts = errors.filter(TimeoutError.is);

Important

Prefer is over instanceof. When a class is declared via class Err extends errorClass(...) {}, error instanceof Err returns false, so is is the reliable way to check the error type.

errorClassWithData

A function that creates a new error class with typed data. It enhances the result of calling the errorClass function.

import { errorClassWithData } from 'error-kid';

class TimeoutError extends errorClassWithData<{ duration: number }, [duration: number]>({
  name: 'TimeoutError',
  data: duration => ({ duration }),
}) {}

const error = new TimeoutError(1000);
error.data; // { duration: 1000 }

TimeoutError.is(error); // true

Note

Unlike errorClass, this function accepts the data type as its first generic parameter, and the constructor arguments tuple as the second one.

This function accepts the same message and cause options as the errorClass function, and they behave exactly the same way.

import { errorClassWithData } from 'error-kid';

class TimeoutError extends errorClassWithData<
  { duration: number },
  [duration: number, cause?: unknown]
>({
  name: 'TimeoutError',
  data: duration => ({ duration }),
  message: duration => `Timed out: ${duration}ms`,
  cause: (_duration, cause) => cause,
}) {}

const error = new TimeoutError(1000, new Error('Just because'));
error.data; // { duration: 1000 }
error.message; // "Timed out: 1000ms"
error.cause; // Error('Just because')

The is predicate narrows the data property as well:

try {
  // ...
} catch (error) {
  if (TimeoutError.is(error)) {
    error.data.duration; // number
  }
}

License

MIT

About

A tiny utility to create custom types of errors.

Resources

Stars

9 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages