promise-utils 
A simple package for a functional and typesafe error handling
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
PromiseOptions
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 PromiseOptions
:
const goFetchData = await go(fetchData('users'), { retries: 2, retryDelayMs: 500, timeoutMs: 5_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:
GoResult<T> = { data: T; success: true }
GoResultSuccess<E extends Error = Error> = { error: E; success: false }
GoResultError<T, E extends Error = Error> = GoResultSuccess<T> | GoResultError<E>
PromiseOptions { readonly retries?: number; readonly retryDelayMs?: number; readonly timeoutMs?: number; }
The default values for PromiseOptions
are:
{ retries: 0, retryDelay: 200, timeoutMs: 0 }
By default, the timeoutMs
value of 0
means that there is no timeout limit.
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.
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.