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
shumway
Surely there must be shumway to make error handling easier!
Safe and simple to use
- 🕵️♀️ Thoroughly tested
- 🕮 Well documented
- ✨ No additional dependencies (except debug)
- 😊 Uses Semantic Versioning and keeps a nice Changelog
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 mustreturn
the new error, notthrow
it. The callback is invoked with the same context as the wrapped function (unless you use an arrow function).onCallbackError: CallbackErrorAction
(optional, defaults toTHROW_WRAPPED
)
This option controls how errors thrown by the callback are handled.THROW_WRAPPED
: The error is wrapped in anMapError
. 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 tofalse
)
If set totrue
, any error thrown by the callback will propagate to the next handler in the chain. Iffalse
, 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 totrue
.
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 toTHROW_WRAPPED
)
This option controls how errors thrown by the callback are handled.THROW_WRAPPED
: The error is wrapped in anRecoverError
. 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 tofalse
)
If set totrue
, any error thrown by the callback will propagate to the next handler in the chain. Iffalse
, 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 toTHROW_WRAPPED
)
This option controls how errors thrown by the callback are handled.THROW_WRAPPED
: The error is wrapped in anRecoverError
. 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 tofalse
)
If set totrue
, any error thrown by the callback will propagate to the next handler in the chain. Iffalse
, 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.