Share it!

What are JavaScript Promises?

Promises are basically a major improvement of callbacks in JavaScript. Before the introduction of promises, events and callback functions were used to handle asynchronous operations.

However, events were not good at handling asynchronous operations and callbacks had limited functionality and multiple, nested callback functions led to complex, hard to read and maintain code. In JavaScript programming, this problem is called Callback Hell or Pyramid of Doom.

This is where Promises come into the picture. A Promise is an action (at its heart, a function call) which will either be fulfilled or rejected. In case of successful completion, the promise is fulfilled and otherwise (in case of any errors or any other problems), the promise is rejected.

Introduced in ES6, Promises represent a proxy for the eventual completion (or failure) of an asynchronous operation and its resulting value.

As the name suggests a Promise is either kept (resolved) or broken (rejected). A Promise is the ideal choice when we are unsure of whether or not a task (function call, data retrieval, etc.) will be successfully completed.

And, unlike callbacks, promises can be chained, making them much more useful when developing code with asynchronous calls, as they can easily handle multiple asynchronous operations and provide better error handling than callbacks and/or events.

First dive into JavaScript Promises

One important thing about Promises is that they have state. A Promise is always in one of the following states:

  • Fulfilled: Action related to the promise succeeded. In other words, the promise was kept.
  • Rejected: Action related to the promise failed. In this case, the promise is broken
  • Settled: Promise has been fulfilled or rejected.
  • Pending: Promise is still pending i.e not fulfilled or rejected yet.

A pending promise can either be fulfilled with a value, or rejected with a reason (error). When either of these options happens, the associated handlers queued up by a promise’s .then() and .catch() methods are called.

If the promise has already been fulfilled or rejected when a corresponding handler is attached, the handler will be called, so there is no race condition between an asynchronous operation completing and its handlers being attached.

Once a promise resolves to a value, it will always remain at that value and never run again.The same happens upon rejection. The promise will stay rejected and never change its status.

As mentioned above, a handler can be assigned to an already “settled” promise.  In that case the action (if appropriate) will be performed at the first asynchronous chance the JS engine finds.  Note that promises are guaranteed to be asynchronous.

Therefore, an action for an already “settled” promise will occur only after the stack has cleared and a clock-tick has passed. This will affect the order in which handlers will be invoked so developers should always keep this in mind.

In other words, handlers (both for fulfilled and rejected promises) can be attached even after the promise has been settled, and these handlers will also be called. On the other hand, when attaching handlers to an unsettled promise, these will not be invoked until the promise settles (i.e., it is either resolved or rejected).

Simple Promise usage

A promise can be explicitly created using the Promise constructor. The Promise constructor takes only one argument: a callback function with two arguments, resolve and reject.

Inside the callback, you can perform any operations (both synchronous and asynchronous). Then, if everything went well, you call resolve() as a signal of success, exiting the callback. If anything goes wrong inside the callback, you call reject() to indicate that the promise was not kept (it was rejected).

Like throw() in plain old JavaScript, it’s customary, but not required, to reject promises with an Error object. The benefit of using Error objects is that they capture a stack trace, making debugging tools more helpful.

Consuming promises

Up to this point, the promise was not executed, it was only defined along with its own executor callback. In order to make use of the promise, developers need to “consume” them. This is accomplished by invoking the promise’s built-in .then() and .catch() methods, as shown in the code above.

The .then() method is ALWAYS called (both when the promise is fulfilled and when it’s rejected) and takes 2 parameters, both being functions. The first function is invoked when the promise is fulfilled (it was successful), while the second is called when the promise was rejected (it failed).

Meanwhile, the .catch() method is called when a promise is either rejected or some error has occurred, either on the executor or any function invoked by it. In this regard, it is very similar to the .catch() statements used in try..catch blocks. While you can handle both a Promise’s success and failure using the .then() method, it’s better to handle failures using .catch(), as it leads to cleaner and easier-to-read code.

Moreover, since promises capture the essence of asynchronicity in an object, we can chain them, map them, have them run in parallel or sequential, all kinds of useful options.

We need to know what state they are in before proceeding, and make sure we move through the states correctly.

  • A promise can be pending waiting for a value, or resolved with a value.
  • Once a promise resolves to a value, it will always remain at that value and never resolve again.

What do Promises return?

Once a Promise is fulfilled or rejected, the respective handler function (.then() / .catch()) will be called asynchronously (scheduled in the current thread loop). The behavior of the handler functions follows a specific set of rules, as follows:

  • If the Promise executor returns a value, the promise gets resolved with the returned value as its value.
  • If the Promise executor doesn’t return anything, the promise gets resolved with an undefined value.
  • If the Promise executor throws an error, the promise gets rejected with the thrown error as its value.
  • If the Promise executor returns an already fulfilled promise, the promise gets fulfilled with that promise’s value as its value.
  • If the Promise executor returns an already rejected promise, the promise gets rejected with that promise’s reason as its value.
  • If the Promise executor returns another pending promise object, the resolution/rejection of the promise will be subsequent to the resolution/rejection of the promise returned by the handler. Also, the resolved value of the promise will be the same as the resolved value of the promise returned by the handler.

Chaining promises

A common need in asynchronous code is to execute two or more operations back-to-back. That is, running each operation when the previous operation succeeds, and using the result from the previous step. We accomplish this by creating a promise chain.

The .then() and .catch() methods always return a promise. So you can chain multiple .then() calls together, making it very simple to create step-by-step actions, where the value returned from each step is used/transformed by the next one.

It should be rather obvious that there’s no need of JavaScript Promises in the example above, as all operations are synchronous and there’s no chance of running into errors. It’s purpose is to show how promise chaining is implemented.

Catching errors in the Promise chain

In the following example, 4 tasks are chained to fetch a random image (from https://picsum.photos) and display it on the page. One of these tasks is a size-validation step. If the picture is smaller than 6KB, the image will be displayed along with its size.

If, however, the picture size is 6KB or larger, the image will be rejected (breaking the promise chain), generating an error that will be caught by the .catch() method at the end of the chain.

Promises support Error propagation so if there’s an exception, the browser will look down the chain for the closest .catch() handler or the first onRejected callback from a .then() handler. In the following example, the .catch() method will handle any errors that might happen in any of the links of the Promise chain.

Sweetening JavaScript Promises

Promises are incredibly powerful and solve many programming problems. They also let us write sequential asynchronous JavaScript that doesn’t block threads while waiting for a response.

However, this comes with a price: promises themselves don’t look a lot like normal JavaScript and they still require developers (and testers) to keep in mind their asynchronous nature, as well as all the possible handler interactions and execution order.

Wouldn’t it be nice to write code that looks synchronous and that would let us use normal language features like try...catch as well? Well, enter ES2017’s async/await feature.

Async functions let us tag functions as asynchronous, and these functions will then return promises, eventually resolving to whatever value is returned (or rejecting with whatever error is thrown).

The await expression causes async function execution to pause until a Promise is settled (that is, fulfilled or rejected), and to resume execution of the async function after fulfillment. When resumed, the value of the await expression is that of the fulfilled Promise.

If the Promise is rejected, the await expression throws the rejected value. Finally, if the value of the expression following the await operator is not a Promise, it’s converted to a resolved Promise., returning the result of the expression.

We can then call these functions and, using the await operator will stop execution until the resolution or failure of those promises. Using this “syntactic sugar”, we can write code that looks more synchronous and “linear” than it really is.

One common mistake is to try using await in top-level code. This will not work and will end in a syntax error, as await can only be used inside asynchronous functions. However, we can use a simple trick and wrap the await call into an anonymous async function (as shown in the example above).

Key advantages of promises

To sum up, there are four key advantages that JavaScript promises have, when compared with callback handlers (continuation-passing style).

  • Better definition of control flow of asynchronous logic: Promise chaining provides a better way to define “what’s next” when dealing with multiple asynchronous calls. Another benefit is being able to write a sequence of N async calls without ending up with N levels of indentation, preventing “callback hell”.
  • Improved readability: First of all, Promises reduces the amount of nested code. This is huge step for improving readability. Also, promise chaining makes it much clearer how the code is supposed to flow, adding to readability. Finally, when combined with async/await, promises makes the code look much more “linear”, making it easier to understand.
  • Reduced coupling: In essence, a promise is an object that represents the result of an asynchronous operation. This means that you can pass it around, proving more flexibility to coders. When using callbacks, you must specify how it will be handled at the time of the invocation of the asynchronous operation, increasing code coupling. With promises, on the other hand, you can specify how it will be handled later. In fact, you can register them AFTER the event and it will get called anyway.
  • Better error handling: Whenever there is an error, exception or Promise rejection, we’re certain that it will be thrown in the next catch handler. This means that you can put catch handler anywhere in the chain to catch specific exceptions, making it much simpler to handle them. Also, Promises let you handle all errors at once at the end of the chain, using a single catch block.

Share it!