acall
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.
Motivation
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:
{ await ; const result = shouldDoThat ? await : null; if result await ; }
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 { // do something} { await }
Some people just get away by doing this:
{ // do something} { ;}
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:
{ await ; ;} // This function gets attached to a button/React onClick/Vue @click { ;}
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:
{ await ;} app;
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:
{ await ; ;} // This function gets attached to a button/React onClick/Vue @click { ;}
However, using acall
just like this is not really that useful. acall
works
best if you provide an error handler:
{ // Show a modal about the error, maybe log error to external service} { await ; ;} // This function gets attached to a button/React onClick/Vue @click { ;}
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.
Installation
$ npm i acall
Usage
; ;
API
acall
exports a single function acall
.
acall(value, [errorHandler])
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):
{ if typeof value === 'function' return ; if valuethen return value; return Promise;}
Recipes
With the above rules, there are a number of useful things we can do with
acall
.
Performing async tasks at browser event handling
TBD
Using async functions as Express middlewares
TBD
main()
entry point for Node.js scripts
Simple TBD
Handling a subset of errors
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:
{ ;} const myAcall = ; ;
Alternatives
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;
Global unhandled 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;
With Node.js you can listen for uncaughtException
event:
process; ; // Intentionally cause an exception, but don't catch it.;console;
Please note that
uncaughtException
also catches synchronous uncaught exception, not only unhandled Promise rejection.
Native async frameworks (e.g. Koa)
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:
{ throw 'eep';} app; app;
License
Licensed under MIT License.