In the world of unreliable people and things, JS always has your back. JS will never ghost you leaving unresolved issues. It will always address your problems with fulfilling answer or rejection for a good reason. It can make you a king or queen of the asynchronous world, where your subjects can race, settle, or all resolve. Today we are gonna dive into what promises syntax looks like, how they resolved the callback hell issue, and when you will probably need them.
Introduction
Promises were proposed to become a part of JS ecosystem in The States and Fates document. They were an answer to a popular problem of nested callbacks, named callback hell or pyramid of doom. How big was the problem, you ask. Well, pretty big:
Try to unwrap this crazy burrito code! That’s why promises were introduced with the release of ECMAScript 6 (ES6) in June 2015. In short terms: they are an object that describes the eventual completion and its resulting value that we couldn’t predict when we created the promise, or failure and its reason, of an asynchronous operation. So how does it work?
Three states of your best friend
By default, a promise can only be in three states, and only one at a time. The initial state of the promise is pending. After that, there are two scenarios: either it is fulfilled with value, or rejected for reason. When it’s fulfilled or rejected, it is said that this promise is settled, which means: do whatever you wanna do with what the promise returned. I think this little chart can be helpful to understand and remember those states:
Syntax
Basic syntax is pretty easy. We are invoking the promise constructor and give it a callback with two parameters: resolve and reject. In this scenario, we have to simulate async action, so I used timeout for 1s. We are giving strict instructions on how to proceed in the case of success- resolve with our greetings variable, and failure- reject with an error message.
According to the previous diagram, now we have to deal with the outcome by chaining our promise with .then method if it was successful, or catching the error in case of failure. There is one other possibility: we can use finally method for necessary actions whether it was successful or not. We can also chain those methods as many times as we want.
Concurrency
You are probably wondering: what if there is more than one of them? How can I assure and predict their behavior? Well, there are four static methods to deal with this problem shown in below diagram:
What does it mean? Using .any method you are saying: if any one of them fulfills-> this is a success, but then all of them must fail, to call it a failure.
The actual opposite to this is Promise.all(), if any one of them fails -> it’s a failure, but they all need to succeed to call it a win.
We also have a ‘happy path’, when we need only one win or only one failure to call it with Promise.race().
And finally we have the most restricted method .allSettled, which means that all of the promises must succeed or fail to call a day. Below, you will find an example of usage with Promise.all:
Example usage
Promises are used every day by thousands of developers for
- HTTP requests handling
- file handling
- timeouts and delays
- database operations
- event handling
- parallel execution
I want to show the basic example of http call to a free REST API named Dog API. First of all, we are instantiating a new promise object with a callback that gets two parameters: resolve and reject, exactly as in the syntax example. After that, we use a fetch fn and give it an endpoint URL found in API documentation as an argument. The function itself returns a Promise that resolves to the Response object representing the response to the request. We can then use the then() method on this Promise to handle the response asynchronously. We are making sure, that the response status is ok. Why? Unfortunately, only network errors or other issues preventing the request from completing will cause the fetch Promise to be rejected. Response.ok method is checking if the status code of the response is between 200 and 299. If the response is not ok we can narrow down the cause of not getting the proper data (like 404 not found or 500 server down) and raise an error. If you want to dive deeper into the HTTP errors topic, here is a fun way to learn some: http cats. Now, the json() method of the request interface reads the request body and returns it as a promise that resolves with the result of parsing the body text as JSON (yes, json method is not returning a JSON, it’s returning an object). And now, finally, we can resolve the outer promise with that object, or reject it if the error(network or raised by us http status error) occurs.
At the end, we are just taking the result of our dogPromise and logging the keys of the message object, and in case of error, catching and logging it as well. I strongly suggest running every step of this independently, to fully understand how we are dealing with 3 promises under the hood and what are their results.
Summary
In conclusion, understanding promises is fundamental for any JavaScript developer, especially for beginners diving into asynchronous programming. It's not just about knowing how to use them; it's about grasping their underlying concepts and principles.
Promises offer a powerful abstraction for handling asynchronous operations, providing a cleaner and more readable alternative to callbacks. With promises, developers can write code that is easier to reason about, maintain, and debug. They enable more structured error handling and make it possible to compose asynchronous operations clearly and concisely.
Moreover, a deep understanding of promises is crucial for resolving common issues such as race conditions, where multiple asynchronous operations compete to resolve first. By mastering promise chaining, developers can effectively manage concurrency and ensure that their applications behave as expected. Link for official documentation is here