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.
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
.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).
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
.catch() methods, as shown in the code above.
.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).
.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 (
.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
- 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.
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.
.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.
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.
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 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).
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
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
- 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.