Master JavaScript Promises

The single-threaded, event-loop based concurrency model of JavaScript, deals with processing of events using so-called “asynchronous non-blocking I/O model.” Unlike computer languages such as Java, where events are handled using additional threads, processed in parallel with the main execution thread, JavaScript code is executed sequentially. In order to prevent blocking of the main thread on I/O-bound operations, JavaScript uses the callback mechanism, where asynchronous operations specify a callback – the function to the be executed when the result of an asynchronous operation is ready, while the code control flow continues executing.

Whenever we want to use the result of a callback to make another asynchronous call, we need to nest callbacks. Further, since I/O operations could result in errors, we need to handle errors for each callback before processing the success result. This necessity to do error handling and having to embed callbacks, makes the callback code difficult to read. Sometimes this is referred to as “JavaScript callback hell.”

In order to address this programming anti-pattern, JavaScript offers a mechanism called a Promise. The JavaScript Promise is an object holding a state, which represents an eventual completion (or failure) of an asynchronous operation, and its resulting value.

A promise is always in either pending  or resolved  state. If a promise succeeds it will put in fulfilled state otherwise, it is rejected. Instead of using the original callback mechanism, code using promises creates a Promise object. We use promises typically with two callback handlers – onFulfilled  invoked when the operation was successful and onRejected  called whenever an error has occurred. When the code inside the promise completes executing, the promise is set to either fulfilled  or rejected  state and the appropriate callback is invoked.

Promise states

Promise states

The code should either assign both success and failure callbacks simultaneously as:

promise.then(onFulfilled, onRejected)

Or one-at-a-time, as below:

Failure callback could be also assigned using a catch keyword as:

promise.catch(onRejected)

In addition, if promise code uses a synchronous throw  operator inside, that also sets promise into the rejected  state and executes the onReject  callback.

Recently, JavaScript added async / await  semantics allowing programmers to deal with promises in a more intuitive a way. The word async before a function means one simple thing: a function always returns a promise. If the code has return in it, then JavaScript automatically wraps it into a resolved promise with that value. The keyword await , which could only occur inside an async function, makes JavaScript wait until the promise has been settled and returns its result.

Below is a function resolving a promise, after one second, using async / await  keywords:

Promise usage anti-patterns

We will go over common examples of incorrect Promise usage and show how to handle each situation correctly.

Nested Promises

Let’s examine the below code:

Promises were invented to address the “callback hell” and above example is written in the “callback hell” style. To rewrite the code correctly, we need to understand why the original code was written the way that it was. In the above situation, the programmer needed to do something after results of both promises are available, hence the nesting. We need to rewrite it using Promise.all()  as:

In addition to making the code easier to read, the new version addressed a potential “unhandled promise rejection” problem.

Broken Promise Chain

One of the main reasons promises are convenient to use is “promise-chaining” – an ability to pass the result of a promise down the chain and calling catch at the end of the chain. Let’s examine the below example:

The problem above is with handling of somethingElse() . An error that occurred inside that segment will be lost. Instead, we should rewrite anAsyncCall() as:

By returning the result of the final then() , we ensured that the chaining will now work correctly.

Calling then() multiple times

Let’s review the below snippet:

Although we are allowed to call then()  multiple times, our code is hard to understand since we do not update p  each time before calling then. A better way to write this would be:

Usually, code would chain functions directly, so our fragment becomes:

Mixing Promises and Callbacks

Never invoke a callback inside then() , else you get unhandled promise rejections. We would loose the exception bubbling and errors will not be passed down the chain. Further, the code becomes overly verbose. The below example:

Will raise an unhandled rejection. Let’s promisify the all functions.

If you are unable to write the code without callbacks, use process.nextTick()  or setTimeout()  to break callback out of the promise chain.

Missing catch

JavaScript does not enforce error handling. Whenever programmers forget to catch the error, JavaScript code will raise a runtime exception. The callback syntax however makes error handling more intuitive, as shown below:

Since the callback function signature has error, handling it becomes more natural and a missing error handler is easy to spot. Promises make it easier to forget catching error, because catch()  is optional while then()  is perfectly happy with a single – success handler. To avoid the ugly runtime errors, always finish promise chains with a catch() .

Forgotten Promise

If you are making a promise, do not forget to return it. In the below code, we forget to return the promise in our getUser()  success handler.

As the result, userData is undefined. Further, such code could cause an unhandledRejection  error. The proper implementation should look like:

Promisified synchronous code

Promised are designed to help you manage asynchronous code. Therefore, there are no advantages to using promises for synchronous processing. As per JavaScript documentation: “The Promise object is used for deferred and asynchronous computations. A Promise represents an operation that hasn’t completed yet, but is expected in the future.” What happens if we wrap a synchronous operation in a promise, as below?

The code inside the promise will be delayed, and scheduled for execution only after the syncPromise() function has been invoked. Further, in the above example, we created an additional context, which we are not using. This will make our code slower and consume additional resources, without yielding any benefits. Further, since our function is a Promise the JavaScript engine will skip one of the most important code optimizations meant to reduce our function call overhead – automatic function inlining.

Missing Rejection Handler

Let’s build a simple web service as below:

If this server receives a request for /, it will respond and clean up the buffer as expected. Otherwise, we see an UnhandledPromiseRejectionWarning  in the console. However, this is not even the main problem. Since we are missing a reject, the promise is never settled and the allocated memory is never freed. Adding a catch()  and rejecting solves the problem. We need to rewrite our handler as:

Conclusion

Mastering JavaScript Promises is essential for writing effective and reliable JavaScript code. Interested in making improving your software development skills? Visit our development blog for more best practices, tutorials, and other helpful information.

More from our blog

See all posts