Utility function to avoid UnhandledPromiseRejection
acall
wraps an asynchronous function or Promise to redirect rejections to a
specified error handling function.
I ended up using acall
in a lot of places for my work, so I decided to move it
into a package.
async
/await
is probably The Greatest Big Thingβ’ ever happened with
JavaScript; there are so many tutorials on this out there. At last, programmers
can express asynchronous codes just like how we already used to with procedural,
sequential code:
async function doSomethingAsync() {
await doSomething();
const result = shouldDoThat ? await doThat() : null;
if (result) {
await doSomethingElse();
}
}
There is a catch, though: await
can only exists inside async
, and we have to
call the async
function somewhere in our sequential code:
// This code won't work and the linter will scream at you
async function asyncFn() {
// do something
}
function nonAsyncFn() {
await asyncFunction()
}
Some people just get away by doing this:
async function asyncFn() {
// do something
}
function nonAsyncFn() {
asyncFn();
}
This is dangerous and should be avoided. The problem is that the execution
of asyncFn
is done in a different sequence (think of threads, except not
really), and if that asyncFn
throws an error, nothing can handle that
error.
Here is another example. Imagine if you do this in your browser code:
async function sendCheckout() {
await fetchAPI({ product: checkoutThings, userInfo: await getUserInfo() });
showNotification('success');
}
// This function gets attached to a button/React onClick/Vue @click
function onButtonCheckoutClick() {
sendCheckout();
}
In the example above, if fetchAPI
throws an error then nothing can handle
that error. Visually nothing will happen, but a PromiseRejectionEvent
will
show up in the console.
Here is another example with Express:
async function handleCheckout(checkoutData) {
await storeCheckoutData(checkoutData);
}
app.use((req, res, next) => {
handleCheckout(req.body);
res.sendStatus(200);
});
If storeCheckoutData(checkoutData)
throws an error, the returned response is
still HTTP 200
as usual, but an ugly UnhandledPromiseRejectionWarning
will
pop up on log:
(node:10047) UnhandledPromiseRejectionWarning: DatabaseError: connection failed
<stack trace>
(node:10047) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:10047) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
The examples might seem contrived, but with large codebases or frameworks with complex JavaScript-fu, tracking which code does not handle the promise rejection is really a painful process.
This is where acall
comes in. You can wrap your async function calls with
acall
to avoid unhandled promise rejection:
async function sendCheckout() {
await fetchAPI({ product: checkoutThings, userInfo: await getUserInfo() });
showNotification('success');
}
// This function gets attached to a button/React onClick/Vue @click
function onButtonCheckoutClick() {
acall(sendCheckout());
}
However, using acall
just like this is not really that useful. acall
works
best if you provide an error handler:
function showErrorNotification(error) {
// Show a modal about the error, maybe log error to external service
}
async function sendCheckout() {
await fetchAPI({ product: checkoutThings, userInfo: await getUserInfo() });
showNotification('success');
}
// This function gets attached to a button/React onClick/Vue @click
function onButtonCheckoutClick() {
acall(sendCheckout(), showErrorNotification);
}
This way, if fetchAPI
throws an error -- be it a HTTP request error or you
just forget to send the authentication token -- showErrorNotification
will
handle that error and show some UI to user, perhaps a modal with message.
There are more recipes on things you can do with acall
below. You
might want to check the Alternatives section as well, because
there are some cases when you might not need acall
at all.
$ npm i acall
import acall from 'acall';
acall(async () => {
await doSomethingAsync();
});
acall
exports a single function acall
.
Given a value
, perform some actions and returns a Promise
. The behaviour of
acall
differs depending on the value:
- If
value
is a function,acall
will callvalue
without any arguments.acall
will returnacall(value())
. - If
value
is a thenable,acall
will returnvalue.then(undefined, errorHandler)
. - Otherwise,
acall
returnsPromise.resolve(value)
.
errorHandler
is optional, but it is recommended to provide one because every
applications handle errors differently.
- In browser environment: call
alert(error.message)
andconsole.error(error)
. - In Node.js environment: call
console.error(error)
and exits the application.
This behaviour is intentional to encourage users for providing their own error handling function.
Put it simply, acall
is just this function (without safety checks and
comments):
function acall(value, errorHandler = defaultErrorHandler) {
if (typeof value === 'function') {
return acall(value(), errorHandler);
}
if (value.then) {
return value.then(undefined, errorHandler);
}
return Promise.resolve(value);
}
With the above rules, there are a number of useful things we can do with
acall
.
TBD
TBD
TBD
Sometimes you want to handle a subset of errors differently. For example, if the
error is a HTTPError
you want to print a connection failure message. In that
case, use a try-catch
block inside acall
to handle the necessary errors:
function myErrorHandler(error) {
showNotification('error', 'Something bad happened');
}
const myAcall = (value, handler = myErrorHandler) => acall(value, handler);
myAcall(async () => {
try {
await doSomethingAsync();
} catch (error) {
if (error instanceof HTTPError) {
showNotification('error', 'Failed to connect with API server');
return;
}
throw error;
}
});
Sometimes you do not need acall
. Basically you do not need to use acall
if a
mechanism to catch unhandled promise rejection already exists. This means that
you do not need to wrap your asynchronous functions; you can simply throw
errors normally.
You can try returning a rejected promise in your code and see if anything handled the error:
(() => Promise.reject(new Error('uncaught promise rejection')))();
Some environments provide a mechanism to handle unhandled promise rejection If
you are allowed to use them or someone already handled this for you, you
probably do not need acall
.
With browsers you can listen for unhandledrejection
event:
window.addEventListener('unhandledrejection', (event) => {
console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
});
With Node.js you can listen for uncaughtException
event:
process.on('uncaughtException', (err, origin) => {
fs.writeSync(
process.stderr.fd,
`Caught exception: ${err}\n` + `Exception origin: ${origin}`
);
});
setTimeout(() => {
console.log('This will still run.');
}, 500);
// Intentionally cause an exception, but don't catch it.
nonexistentFunc();
console.log('This will not run.');
Please note that
uncaughtException
also catches synchronous uncaught exception, not only unhandled Promise rejection.
If you use frameworks that already support Promise/async
, you probably do not
need acall
. acall
is intended as an interface to call asynchronous functions
from non-asynchronous code; if your framework already do this, there are no need
to use acall
.
Koa, for example, has a built-in error-handling mechanism:
async function doSomething() {
throw new Error('eep');
}
app.use(async (ctx) => {
ctx.body = await doSomething();
});
app.on('error', (err, ctx) => {
log.error('server error', err, ctx);
});
Licensed under MIT License.