promise-utils
A simple package for a functional and typesafe error handling with zero dependencies
Installation
To install this package run either:
yarn add @api3/promise-utils
or if you use npm:
npm install @api3/promise-utils --save
Usage
The API is small and well focused on providing more concise error handling. The main functions of this
package are go
and goSync
functions. They accept a function to execute, and additionally go
accepts an optional
GoAsyncOptions
object as the second parameter. If the function executes without an error, a success response with the
data is returned, otherwise an error response is returned.
const goFetchData = await go(() => fetchData('users'));
if (goFetchData.success) {
const data = goFetchData.data
...
}
or:
const goFetchData = await go(() => fetchData('users'));
if (!goFetchData.success) {
const error = goFetchData.error
...
}
and with GoAsyncOptions
:
const goFetchData = await go(() => fetchData('users'), { retries: 2, attemptTimeoutMs: 5_000, totalTimeoutMs: 10_000 });
...
and for synchronous functions:
const someData = ...
const goParseData = goSync(() => parseData(someData));
if (goParseData.success) {
const data = goParseData.data
...
}
The return value from the promise utils functions works very well with TypeScript inference. When you check the the
success
property, TypeScript will infer the correct response type.
API
The full promise-utils
API consists of the following functions:
go(asyncFn, options)
- Executes the asyncFn
and returns a response of type GoResult
goSync(fn)
- Executes the fn
and returns a response of type GoResult
assertGoSuccess(goRes)
- Verifies that the goRes
is a success response (GoResultSuccess
type) and throws
otherwise.assertGoError(goRes)
- Verifies that the goRes
is an error response (GoResultError
type) and throws otherwise.success(value)
- Creates a successful result value, specifically {success: true, data: value}
fail(error)
- Creates an error result, specifically {success: false, error: error}
and the following Typescript types:
-
type GoResult<T> = { data: T; success: true };
-
type GoResultSuccess<E extends Error = Error> = { error: E; success: false };
-
type GoResultError<T, E extends Error = Error> = GoResultSuccess<T> | GoResultError<E>;
-
interface GoAsyncOptions<E extends Error = Error> {
retries?: number;
attemptTimeoutMs?: number | number[];
totalTimeoutMs?: number;
delay?: StaticDelayOptions | RandomDelayOptions;
onAttemptError?: (goRes: GoResultError<E>) => void;
}
-
interface StaticDelayOptions {
type: 'static';
delayMs: number;
}
-
interface RandomDelayOptions {
type: 'random';
minDelayMs: number;
maxDelayMs: number;
}
Careful, the attemptTimeoutMs
value of 0
means timeout of 0 ms. If you want to have infinite timeout omit the key or
set it to undefined
.
The last exported value is a GoWrappedError
class which wraps an error which happens in go callback. The difference
between GoWrappedError
and regular Error
class is that you can access GoWrappedError.reason
to get the original
value which was thrown by the function.
Take a look at the implementation and
tests for detailed examples and usage.
Motivation
Verbosity and interoperability of try-catch pattern
try {
const data = await someAsyncCall();
...
} catch (e) {
return logError((e as MyError).reason);
}
const goRes = await go<MyData, MyError>(someAsyncCall);
if (!goRes.success) return logError(goRes.error.reason);
const data = goRes.data;
...
Also, think about what happens when you want to handle multiple "can fail" operations in a single function call. You can
either:
- Have them in a same try catch block - but then it's difficult to differentiate between what error has been thrown.
Also this usually leads to a lot of code inside a try block and the catch clause acts more like "catch anything".
- Use nested try catch blocks - but this hurts readability and forces you into the
callback hell pattern.
Consistent throwing of an Error
instance
JavaScript supports throwing any expression, not just Error
instances. This is also a reason why TypeScript infers the
error as unknown
or any
(see:
useUnknownInCatchVariables).
The error response from go
and goSync
always return an instance of the Error
class. Of course, throwing custom
errors (derived from Error
) is supported.
Intentionally limited feature set
The go utils by design offer only very basic timeout and retry capabilities as these are often application specific and
could quickly result in bloated configuration. If you are looking for more complex features, consider using one of the
alternatives, e.g. https://github.com/lifeomic/attempt
Limitations
There is a limitation when using class functions due to how javascript
this works.
class MyClass {
constructor() {}
get() {
return this._get();
}
_get() {
return '123';
}
}
const myClass = new MyClass();
const resWorks = goSync(() => myClass.get());
const resFails = goSync(myClass.get);
The problem is that the this
keyword is determined by how a function is called and in the second example, the this
inside the get
function is undefined
which makes the this._get()
throw an error.
Developer documentation
Release
To release a new version follow these steps:
yarn && yarn build
yarn version
and choose the version to be releasedyarn publish --access public
git push --follow-tags