Async Await - Promises but more Readable
Whenever developers would write promises, they would thank them for existing as a way to avoid callback hell. But that was for the generation that wrote callbacks or used the Async.js library. Over time, even promises began to burn out developers because of the explicit nature of the syntax. It mainly didn't feel instinctive. It was a concept that one had to learn very deliberately. In the world of coding, the more intuitive the code is, the fewer chances of errors. Hence, the concept of async await was introduced.
The Syntax for async-await
The usage of async-await is very simple because, well, it was designed to make things simple. First of all, to use this, we would need a function. At the beginning of defining the function, we append the keyword "async". Now this function has some restrictions and some abilities. This function will always return a promise. If it returns nothing explicitly, it resolves with undefined. The advantage is that now we can use the await keyword inside it. But what does it actually do?
Let us see an example:
async function getData(url){
const res = await fetch(url);
const data = await res.json();
return data;
}
Now, as we know, fetch is an asynchronous function. So normally, the data returned would not be reliable. But here we are using the await keyword. The job of the await keyword is pretty straightforward. It can be thought of as suspending the async function's continuation until the awaited promise settles. It is important to note that await is generally used inside async functions, though modern JavaScript modules also support top-level await.
This would mean that we can do things like:
async function getData(userid){
const user = await db.getUser(userid);
// Execution paused until user is received
user.voterRight = user.age >= 18;
await db.save(userid, user);
return user;
}
In this code, we can chain operations without any visible chaining method. The flow is very obvious. Since we can write await and resume only after the previous asynchronous step has settled, we can write some functions that would generally become very complex to implement using normal .then() chaining. Like:
async function getData(urls){
for (const url of urls) {
const res = await fetch(url);
const data = await res.json();
console.log(data);
}
}
But if you can sense it, something is wrong. JavaScript doesn't pause execution like that. And fetch returns a promise. This should use .then(). What are we even doing? And you are right. This should! To understand what is happening, we will need to look deeper.
Unmasking Async Await
Async-Await is not a different concept from promises; as promises were to callback hell. It is built on top of promises only. First of all, we need to understand that async is nothing but making the function ready for the kind of tasks that we are going to perform. Now we just need to understand await.
The keyword await is used with promises and thenables. A thenable is any object that has a .then() method. If such a value has not settled yet, await pauses the async function and resumes it later when the value is fulfilled or rejected.
As we know, await works with promises and thenables (objects having a .then() method) like:
const data = await fetch(url);
Await unwraps the fulfilled value of the promise and assigns it to the variable. This is what makes the syntax easy to use and widely adopted.
It is important to note that pausing execution does not mean blocking the thread. While the async function is suspended, the event loop can continue processing other tasks from the microtask queue or task queue. Hence, the way it works is closely related to promises. Most async/await flows can also be represented using .then() chains.
For example, let us take this code, for example, where we try to print how many seconds have passed since the start of this code's execution.
async function count(){
let i =0;
while(true){
await new Promise((res,rej)=>{
setTimeout(()=>{
console.log(i++);
res();
},1000);
});
}
}
count();
This code is very easy to understand because of the async await syntax. But we can achieve the same functionality using .then.
function run(i){
return new Promise((res,rej)=>{
setTimeout(()=>{
console.log(i++);
res();
},1000);
}).then(()=>run(i));
}
async function count(){
let i =0;
run(i);
}
count();
Even though the process is the same, the previous code is far easier to think of than this one.
Error Handling with Async Await
Since the main reason for introducing async-await was to make the code look more like synchronous JavaScript, handling errors is also managed here in a similar manner. We use try-catch.
async function getData(url){
try{
const res = await fetch(url);
const data = await res.json();
return data;
}catch(error){
console.log(error);
}
}
We can also do multiple awaits in a single try block, and all their problems can be caught within the same catch block.
async function getData(userid){
try{
const user = await db.getUser(userid);
user.voterRight = user.age >= 18;
await db.save(userid, user);
return user;
}catch(error){
console.log('Error in performing DB operations. Exiting...');
return;
}
return user;
}
Conclusion
Async Await is usually a very simple concept to grasp once one has a fine understanding of promises. After that, async await does not feel mystical. In fact, they feel tiny in front of the complexity one would need to deal with to understand their significance. But it is this complexity that is important for engineers, because code can be written by AI, too. And the reason behind this is that, maybe one day you can be the one who creates something better than even async-await?


