Node.js is great, and we use it at work (at Lusha). But every once in a while, it offers a mystery…  

Recently we encountered an interesting case while monitoring our systems. We noticed the famous “unhandledRejection“ event that appeared as part of our k8s readiness check.

After spending a good amount of time trying to debug and do some investigative work to understand what happened, we couldn’t reproduce the warning. We added try/catch blocks but still, nothing. Chances are we missed handling a rejected promise in some way.

Basically, what we were trying to figure out was when does the event get emitted?

I’ll elaborate on our findings – but TL;DR – I’ve summarized it for you here! (you can thank me later)

Let’s Look for the Answers in the Docs

Node.js’s official documentation states the following:

“The “unhandledRejection“ event is emitted whenever a promise is rejected and no error handler is attached to the promise”

That’s kind of obvious. But wait, if you keep reading:

“within a turn of the event loop.”

What? What exactly is a “turn” of the event loop? We needed to investigate further.

Ok, Let’s Search Stack Overflow!

Running a search for unhandledRejection yielded the following:

Someone must be got it right. let’s view some questions & answers:

Wow! Too many answers, too much confusion. we weren’t getting anywhere.

Seems Stack Overflow didn’t help after all. 

No Other Choice, We Had to Go to Node Internals for Answers

First off, we had to zoom out of our particular problem and look at the bigger picture – the EVENT LOOP:

The white boxes represent the queues (macro-tasks) of the different phases, and the two yellow boxes in the middle represent micro-task queues that must be completed before Node continues from one phase to the other ( micro-tasks are basically promise callbacks and callbacks that get passed to process.nextTick).

Now that we have the flow in front of us, we can continue in our search for an answer. 

Let’s open the Node.js project.

Let’s take a look at promises.js, and find the function unhandledRejection:

This function will be called whenever a promise is rejected and no error handler is attached to the promise.

In line 4 you’ll find the “emit” function which emits the “unhandledRejection” event in question. 

In line 13, we found that the “emit” function and the rejected promise have been added to a WeakMap named “maybeUnhandledPromises” (a collection of key/value pairs in which the keys are weakly referenced).

A ‘Maybe’ Status for Unhandled Promises

Ok, so when did we loop through those promises and emit the warning?

In the same file, we found processPromiseRejections:

In line 8, we needed to check that as of this moment, every promise in “maybeUnhandledPromises” was already handled. In other words, everything that may have been an unhandled promise up to this point, has been handled.

If it wasn’t, we needed to emit the warning and take action based on the unhandledRejectionMode (Today the default is to only emit a warning, but from V15 it will terminate the Node.js process with a non-zero exit code).

Our next step was to figure out when to call processPromiseRejections.

Remember those micro-task queues we discussed earlier? Well, they get processed in between two phases of the event loop when libuv communicates back to higher layers of Node at the end of a phase.

That magic happens in task_queues.js, inside the function processTicksAndRejections:

Recap This Whole Thing for Me

Sure, here it is again.

Whenever a promise is rejected and no error handler is attached to it, the promise gets added to a list of “maybe” unhandled promises. Then, between two phases of the event loop, all micro-tasks will execute before Node continues from one phase to the other.

By that point, Node will process all promise rejections. If no handler is attached to the rejected promise, the event will get emitted.

In the code above you can see how we looped over the micro-tasks from the queue and invoked them with the provided arguments. And then in line 27 you can see the final result – when the queue is empty the processPromiseRejections gets called, which eventually emits the “unhandledRejection” warning 🎉.

Don’t be afraid to open the source code and find your answers

We encountered a problem and went about fixing it by checking the official documentation first. When that failed to give us enough answers, we went to Stack overflow, which is where we all go for community help and support. But as great as Stack overflow is, sometimes our peers there create more noise than simple answers. Exhausting our options, we needed to dive into our source code for the answer. And guess what? It really wasn’t that difficult!

Hope this inspires you to use your code to guide you through problems.

4.1 9 votes
Article Rating
Subscribe
Notify of
guest
2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

Developer? Join Lusha - Apply today!