We wrote Asynchronous JS using Callbacks extensively a while back.
But things changed in 2015 when ES6 brought with it a Javascript Promise Object.
Since then most of the Developers now prefer Promises over Callbacks.
What caused this shift? To understand that let's understand what are the problems with Callbacks.
Drawbacks of Callbacks
Dangers of Relying on Third Party
When we use a third-party library we are forced to rely on them and hope that they don't mess up.
There is a very good example of this situation in the book "You don't know JS Async" where a credit card of a person was debited 5 times on a single transaction because the third party called a callback function 5 times accidentally.
The problems that we might face if we rely on third parties.
- Callbacks are called Synchronously
- Callbacks are called multiple times
- Callbacks are never called
Callback Hell (Nested Callbacks)
If we want to execute some functions, after the execution of an async function and then get notified about its completion we need to use the Callback pattern.
That's when the callback nesting happens. We go on adding callbacks in a nested order
It looks like this.
These Nested Callbacks have two major problems.
Error Handling
We need to handle errors in every callback and it's repetitive. This code isn't DRY and definitely not clean.
Parallelism
Asynchronous functions can be executed in two ways
- serial execution ( When we want to read a file and then write it's content in another file)
- parallel execution (When we want to read two files and make sure the content of one file is displayed before the other) But we can't achieve parallel execution using Callbacks. Callbacks restrict us to run in serial.
Even if we gatekeep the reading of the files and then display the files one by one(controlling the race condition) we can't use this technique in production as it's not scalable, flexible, or maintainable.
Now that we understand that Callbacks are a little messed up let's move on to "Promises" and see how these objects solved what callbacks couldn't.
What are Promises
According to MDN docs
The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.A Promise is a proxy for a value not necessarily known when the promise is created. It allows you to associate handlers with an asynchronous action's eventual success value or failure reason. This lets asynchronous methods return values like synchronous methods instead of immediately returning the final value, the asynchronous method returns a promise to supply the value at some point in the future.
Promise States
A Promise can be in one of these states:
- pending: initial state where the promise is neither fulfilled nor rejected.
- fulfilled: this state means that the operation was completed successfully.
- rejected: this means that the operation has failed.
A pending promise can either be fulfilled with a value or rejected with a reason (error). A promise is said to be settled if it is either fulfilled or rejected, but not pending.
How to create a promise?
- We use the Promise() syntax for the creation of a new promise object.
const promise=new Promise();
How to fulfill or reject a promise?
The promise constructor function accepts one function as an argument. This function has two arguments - resolve & reject.
const promise=new Promise((resolve,reject)=>
{
setTimeout(()=>{
if(some condition){
//change status from pending to fulfilled
resolve("resolved");
}
else
{
//change status from pending to rejected
reject("rejected");}
}},1000);
});
How to execute a callback function on the status change of the promise?
const onFulfill=()=>console.log("Fulfilled");
const onReject=()=>console.log("Rejected");
then() & catch()
The promise object has two functions that can be used after the promise is resolved or rejected to perform the next steps. A callback function can be passed to them as arguments to execute after the promise has been executed.
When the Promise status is changed from pending to fulfilled .then() is invoked and when status changes from pending to rejected .catch() are invoked.
We can pass some data using reject and resolve. The promise will automatically inject the data passed, to the onFulfill and onReject functions as arguments.
const promise=new Promise((resolve,reject)=>
{
setTimeout(()=>{
if(some condition){
//change status from pending to fulfilled
resolve("resolved");
}
else
{
//change status from pending to rejected
reject("rejected");}
}},1000);
});
const onFulfill=(result)=>console.log(result);
const onReject=(error)=>console.log(error);
promise.then(onFulfill);
promise.catch(onReject);
Promise Chaining
then() and catch() can also be chained as below
promise.then(onFulfill).catch(onReject);
Instance Methods
- Promise.prototype.catch()
- Promise.prototype.then()
- Promise.prototype.finally()
Static Methods
- Promise.all(iterable of promises) Query multiple APIs and perform actions after all the APIs have finished loading.
const promise1=Promise.resolve(1);
const promise2=2;
const promise3=
new Promise((resolve,reject)=>setTimeout(resolve,1000,3));
Promise.all([promise1,promise2,promise3]).then((values)=>console.log(values));
//Expected Output : [1,2,3,]
It resolves only when all the promises have been resolved and rejected as soon as one of the rejects and returns the first rejection message/error.
Promise.allSettled(iterable of all Promises) Query multiple APIs and perform actions and waits for all input promises to complete regardless of whether or not one of them is rejected.
Promise.race(iterable) This method returns a promise as soon as one of the input promises reject or resolve.
Promise.reject(reason) Returns a rejected promise
- Promise.resolve(value) Returns a resolved promise
for example
let myFirstPromise = new Promise((resolve, reject) => {
// We call resolve(...) when what we were doing asynchronously was successful, and reject(...) when it failed.
// In this example, we use setTimeout(...) to simulate async code.
// In reality, you will probably be using something like XHR or an HTML5 API.
setTimeout( function() {
resolve("Success!") // Yay! Everything went well!
}, 250)
})
myFirstPromise.then((successMessage) => {
// successMessage is whatever we passed in the resolve(...) function above.
// It doesn't have to be a string, but if it is only a succeed message, it probably will be.
console.log("Yay! " + successMessage)
});
async / await
More recent additions to the JavaScript language are async functions and the await keyword, added in ECMAScript 2017. These features basically act as syntactic sugar on top of promises, making asynchronous code easier to write and to read afterward.
The async/await keywords allow us to write completely synchronous-looking code while performing asynchronous tasks behind the scenes.
async
- The async keyword is used to declare async functions.
- An async function always returns a promise.
- An async function is a function that knows how to expect the possibility of the await keyword being used to invoke asynchronous code.
function name() { return "Aniket" };
name();
name() returns "Aniket" here, but if we use async keyword here
async function name(){return "Aniket"}
name()
a promise object will be returned. The returned values are converted to promises. The return value of an async function is implicitly wrapped in Promise. resolve - if it's not already a promise itself (as in the examples).
- An async function expression can also be written as below
let name=async function(){return "Aniket"}; name();
Async function using arrow functions
let name=async ()=>"Aniket";
As the async function returns a promise object the below code will give the same result as the above code
let name=async function(){return Promise.resolve("Aniket")}; name();
To actually consume the value returned when the promise is fulfilled since it is returning a promise, we can use a .then() but the real advantage of an async function only becomes apparent when you combine it with the await keyword.
await
- await only works inside async functions within regular javaScript code.
- await keyword can be used in front of any async promise-based function to pause your code until that promise settles and returns its result.
- await makes the code look even more synchronous and we can use try{} catch{} blocks with it to catch errors
- await can be used when calling any function that returns a promise, including web API functions.
We can use async-await to run Sequential, Concurrent, and Parallel Execution, unlike callbacks.
Sequential Execution
const resolveOneSecond=()=>{
return new Promise(resolve=>
{
setTimeout(()=>resolve("one second"),1000)
}
}
const resolveTwoSecond=()=>{
return new Promise(resolve=>
{
setTimeout(()=>resolve("two second"),2000)
}
}
(async ()=>{
const printOne=await resolveOneSecond();
console.log(printOne);
const printTwo=await resolveTwoSecond();
console.log(printTwo);
)()
//This takes three seconds to complete
Concurrent Execution
const resolveOneSecond=()=>{
return new Promise(resolve=>
{
setTimeout(()=>resolve("one second"),1000)
}
}
const resolveTwoSecond=()=>{
return new Promise(resolve=>
{
setTimeout(()=>resolve("two second"),2000)
}
}
(async ()=>{
const printOne= resolveOneSecond();
const printTwo= resolveTwoSecond();
console.log(await printTwo); //logs after 2 seconds
console.log(await printOne); //logs after 2 seconds
)()
//This takes two seconds to complete as by the time printTwo is ready printOne has already resolved and waiting.
Parallel Execution
const resolveOneSecond=()=>{
return new Promise(resolve=>
{
setTimeout(()=>resolve("one second"),1000)
}
}
const resolveTwoSecond=()=>{
return new Promise(resolve=>
{
setTimeout(()=>resolve("two second"),2000)
}
}
(async ()=>{
await Promise.all([
(async ()=> console.log(await resolveOneSecond())(), //Takes one second to log
(async ()=> console.log(await resolveTwoSecond())() //Takes two seconds to log
]);
)()
// Here both the promises will run parallelly and log after their individual time.
Wrapping Up
Let's revise what we learned so far.
- We can say that promises are cleaner and provide us with more control over the asynchronous functions. .then(), .catch() and async/await makes life easier for developers.
- Promises have three states Pending, Fulfilled & Rejected
- There are various Static functions for promises like Promise.race() or Promise.all()
- We can use then() & catch() to chain the promise
- Using async/await we can pause the code execution asynchronously
- Error handling becomes easy with promises
If you like the article or if you have any suggestions for me please get in touch on twitter.com/aniketxparihar at Twitter. Thank You!