JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 315
  • Score
    100M100P100Q103037F
  • License MIT

Reduce boilerplate code when handling exceptions.

Package Exports

  • shumway
  • shumway/dist/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (shumway) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

npm GitHub Workflow Status GitHub Issues libraries.io Codecov npm bundle size Snyk Vulnerabilities for npm package

shumway

Surely there must be shumway to make error handling easier!

Safe and simple to use

Elevator Pitch

Asynchronous calls typically involve some kind of I/O, such as network requests or disk operations, both of which are prone to errors and can fail in all sorts of ways. Oftentimes, especially when in a Clean Architecture, you want to hide implementation details - in this case errors - from the rest of the application.

In order to do so, it is commonly considered good practice to throw more "abstract" errors, so maybe a custom RemoteFooProviderError instead of just letting the error thrown by the used HTTP client bubble up. This is frequently combined with adding some additional logic like so:

public async getFooFromRemoteSource(id: number): Promise<Foo> {
    let response: FooResponse

    try {
        response = await this.httpClient.get(`/foo/${id}`)
    } catch (error) {
        if (error instanceof HttpClientError) {
            if (error.statusCode === 404) {
                throw new FooNotFoundError(id)
            }

            throw new FooRemoteSourceError(error)
        }

        throw new UnexpectedFooRemoteSourceError(error)
    }

    return new Foo(response.data)
}

While there is nothing wrong with that approach per se, it quickly becomes tedious (and increasingly annoying to maintain) once you have multiple methods that share the same error handling boilerplate. It is also not particularly nice-looking code because the signal-to-noise-ratio is fairly poor.

This is the problem this module sets out to solve:

@HandleError(
    {
        action: HandlerAction.MAP,
        scope: HttpClientError,
        predicate: error => (error as HTTPError).statusCode === 404,
        callback: (_error, id) => new DeviceNotFoundError(id),
    }, {
        action: HandlerAction.MAP,
        scope: HttpClientError,
        callback: error => new FooRemoteSourceError(error),
    }, {
        action: HandlerAction.MAP,
        callback: error => new UnexpectedFooRemoteSourceError(error),
    },
)
public async getFooFromRemoteSource(id: number): Promise<Foo> {
    const response = await this.httpClient.get(`/foo/${id}`)
    return new Foo(response.data)
}

For a more complete (and more realistic) example, check out our use cases, e.g. the API wrapper use case.

Caveats and known limitations

  • This library currently works only with asynchronous functions. A version for synchronous functions could be added later if there is a need for it.
  • By its very nature, decorators do not work with plain functions (only class methods).

Installation

Use your favorite package manager to install:

npm install shumway

Usage

Simply decorate your class methods with @HandleError and configure as needed:

class Foo {
    @HandleError({
        action: HandlerAction.RECOVER,
        callback: () => 'my fallback value',
    })
    public async thouShaltNotThrow(): Promise<string> {
        // ...
    }
}

Remember that a handler is only ever called if the wrapped function throws an error.

Error Handlers

This package provides a variety of handlers (specified by the action property, the examples above).

Common Options

Every handler supports the following options:

  • scope: Class<Error> (optional)
    Limit the handler to exceptions of the given class (or any subclass thereof).
  • predicate: (error, ...parameters) => boolean | Promise<boolean> (optional)
    Dynamically skip invocation of the handler depending on the error thrown or any of the wrapped method's parameters. The predicate is invoked with the same context as the wrapped function (unless you use an arrow function).

MAP

The MAP action allows you to map an error to a different one.

It accepts the following additional options:

  • callback: (error, ...parameters) => Error | Promise<Error> (mandatory)
    The error thrown by the function will be replaced by the error returned by the callback. Note that the callback must return the new error, not throw it. The callback is invoked with the same context as the wrapped function (unless you use an arrow function).
  • onCallbackError: CallbackErrorAction (optional, defaults to THROW_WRAPPED)
    This option controls how errors thrown by the callback are handled.
    • THROW_WRAPPED: The error is wrapped in an MapError. This is the default.
    • THROW: The error is thrown as-is.
    • THROW_TRIGGER: The error is ignored and the triggering error is thrown instead.
  • continueOnCallbackError (optional, defaults to false)
    If set to true, any error thrown by the callback will propagate to the next handler in the chain. If false, that error is thrown immediately and no other handler is called.
  • continue: boolean (optional)
    By default, a mapped error is thrown immediately and no other handler is called. If you want to continue the chain of handlers (now with the mapped error), you can set this parameter to true.

PASS_THROUGH

The PASS_THROUGH action short-circuits the handler chain and causes the current error to immediately be thrown.

It does not accept any additional options.

RECOVER

The RECOVER action suppresses the thrown error and makes the wrapped function return the provided value instead.

It accepts the following additional options:

  • callback: (error, ...parameters) => T | Promise<T> (mandatory)
    The callback must return a value (or Promise) compatible with the return value of the wrapped function. The callback is invoked with the same context as the wrapped function (unless you use an arrow function).
  • onCallbackError: CallbackErrorAction (optional, defaults to THROW_WRAPPED)
    This option controls how errors thrown by the callback are handled.
    • THROW_WRAPPED: The error is wrapped in an RecoverError. This is the default.
    • THROW: The error is thrown as-is.
    • THROW_TRIGGER: The error is ignored and the triggering error is thrown instead.
  • continueOnCallbackError (optional, defaults to false)
    If set to true, any error thrown by the callback will propagate to the next handler in the chain. If false, that error is thrown immediately and no other handler is called.

SIDE_EFFECT

The SIDE_EFFECT action executes the given callback but does not affect the error thrown (if it does not throw itself).

It accepts the following additional options:

  • callback: (error, ...parameters) => Error | Promise<Error> (mandatory)
    The callback is invoked with the same context as the wrapped function (unless you use an arrow function).
  • onCallbackError: CallbackErrorAction (optional, defaults to THROW_WRAPPED)
    This option controls how errors thrown by the callback are handled.
    • THROW_WRAPPED: The error is wrapped in an RecoverError. This is the default.
    • THROW: The error is thrown as-is.
    • THROW_TRIGGER: The error is ignored and the triggering error is thrown instead.
  • continueOnCallbackError (optional, defaults to false)
    If set to true, any error thrown by the callback will propagate to the next handler in the chain. If false, that error is thrown immediately and no other handler is called.

TAP

The TAP action executes the given the callback, ignores errors thrown by it and always throws the original error.

This handler is a shorthand for SIDE_EFFECT with callbackErrorOption set to THROW_TRIGGER and continueOnCallbackError set to true.

This is the only handler that is guaranteed not to alter the behaviour of the wrapped function (unless it explicitly terminates the application, causes an infinite loop, exhausts available memory or causes some other catastrophic error).

It accepts the following additional options:

  • callback: (error, ...parameters) => void | Promise<void> (mandatory)
    The callback is invoked with the same context as the wrapped function (unless you use an arrow function).

Errors thrown in callbacks

If a callback throws, the onCallbackError options configures, how the error handling will proceed.

THROW

The error thrown by the callback is re-thrown as-is.

THROW_TRIGGER

The error thrown by the callback is ignored and the triggering error is re-thrown instead.

THROW_WRAPPED

The error thrown by the callback is wrapped in a SideEffectError, MapError, or RecoverError depending on the action. Note that any error thrown from a TAP callback is always suppressed.

Dos and Dont's

Don't overdo it

If you find yourself writing increasingly complex (and possibly convoluted) error handlers, consider refactoring the code instead. A function throwing all sorts of different errors is a pretty strong indication that it does too much.

Do be careful with side effects

Most of the error handlers of this library don't change the semantics of the wrapped method. The notable exceptions to this are SIDE_EFFECT and RECOVER. It is recommended to use those sparingly in order to avoid surprises and unexpected behavior.

Don't use exceptions for control flow

Using exceptions for control flow is considered an anti-pattern. An exception should, by definition, not be something you encounter during normal operation of your app. As always, use your best judgement.

Debugging

This package uses debug with the shumway prefix.