Unhandled Promise Rejection in depth

Introduction

The origin of the Unhandled Promise Rejection error lies in the fact that every JavaScript Promise is expected to handle the rejection with a catch.

JavaScript Promise

Since JavaScript code won’t crash even when reject handlers are missing, is it important to handle all promise rejections and happens when a rejection is missing? In order to answer this question, we need to look at the V8 – JavaScript engine behind Chrome and Node.JS, the Promise object itself, and how it is implemented whether natively or using a library.

Anatomy of JavaScript code execution

Modern JavaScript code runs inside a JavaScript engine. The most popular JavaScript engine, powering Google Chrome and Chromium browsers as well as Node.JS, is the V8 JavaScript engine – an open source project originally developed by Google in order to improve the performance of JavaScript inside a web browser.

When V8 start executing the JavaScript code, it first leverages full-codegen, which directly translates the parsed JavaScript into machine code without any transformation. This allows V8 to start executing machine code very fast. Please note that V8 does not use intermediate bytecode representation, thus removing the need for an interpreter.

JavaScript engine pipeline

When the code has been running for some time, the profiler thread eventually gathers enough data to decide which methods should be optimized. At that point, Crankshaft optimizations begin in another thread. Crankshaft translates the JavaScript abstract syntax tree into a high-level static single-assignment (SSA) representation called Hydrogen and tries to optimize the resulting Hydrogen graph. This is where most optimizations happen. Most common optimizations are inlining and caching.
Once the Hydrogen graph is optimized, Crankshaft translates it to a lower-level representation called Lithium. Most of the Lithium implementation is architecture-specific.

Promise implementation

Let’s review how event loop schedules resolved/rejected promises (including native JS promises, Q promises, and Bluebird promises) and next tick callbacks.

In the context of native promises, a promise callback is considered to be a microtask. It is queued in a microtask queue, which will be processed right after the next tick queue. Early versions of Node.JS did not have native promises support. In order to run asynchronous code on older versions of Node.JS and/or browsers without promise support, developers could use one of many libraries offering promises, such as Q and Bluebird. While depending on the library and the platform, implementations could use different JavaScript mechanisms e.g. setImmediate vs process.nextTick or setTimeout, the main difference between libraries is what queue will be used to schedule the resolve and reject callbacks.

Since V8 is written in C++ and translates JavaScript to machine code, it is safe to guess that internally promises are represented by an object of sorts. Indeed, looking at V8 sources, we find the Promise class that gets instantiated whenever we create a native JavaScript Promise object. Promise objects are then stored in heap. Whenever we execute accept and/or reject handler, our promise is resolved and the memory allocated for the promise object in question is eventually freed.

While V8 is albeit leading, only one of many JavaScript engines, due to its nature, promises implementations on other platforms are largely similar to the one we already reviewed. Since the very idea of a Promise is that it either succeeds or fails, any engine and/or a library correctly implementing JavaScript Promise will have to keep the object representing eventual completion or failure somewhere until either callback handler was invoked. Since our promise failed, yet we don’t have and therefore never executed the catch/reject callback, the memory allocated for our promise is not freed until the promise object itself is not longer referenced.

Therefore, it is quite clear that each unhandled promise is a potential problem delaying garbage collector from freeing memory. If references to the promise are kept around as well, we are going to leak the memory used by the promise object. Further, missing handlers break promise chains making the code unreliable.

Plugging Unhandled Promise Rejection

If you want to proof Node.JS applications against unhandled promise rejection, one of the simplest ways is by extending your code with the below:

Now, if there will ever be an unhandled promise in the code path, the application will crash providing us with a stacktrace. Using that trace, we could plug each leak, eventually getting to the point when all promises are handled correctly.

Conclusion

Although not strictly prohibited by the runtime, unhandled promise rejections cause memory leaks, among other problems and must be taken quite seriously. One simple way of dealing with unhandled exceptions is by crashing our code while gathering a stacktrace to identify the source of the problem.
Want to learn more about JavaScript promises best-practices and anti-patterns, please review our Mastering JavaScript Promise blog post.

More from our blog

See all posts