Async/Await in JavaScript

If you’ve ever found yourself squinting at code that looks like a sideways pyramid—nested five levels deep with closing braces and parentheses—you’ve likely survived "callback hell." JavaScript has always been the king of non-blocking operations, but for a long time, writing that asynchronous logic was, frankly, a headache.
The introduction of Async/Await in ES2017 didn't just change the syntax; it changed how we think about time in our code. So we would see how it did just that:
Why Async/Await Was Introduced
Before async/await, we had two main ways to handle tasks like fetching data reading files: Callbacks and Promises.
For promises you can refer to this article by clicking here.
Callbacks were the original solution, but they led to deeply nested structures that were nearly impossible to debug, perhaps not nearly impossible but definitely soul crushing. Promises (introduced in ES6) were a massive step up. They gave us .then() and .catch() chains, which flattened the code. However, Promises still felt a bit "functional" and verbose. If you had five dependent asynchronous steps, you were still looking at a long chain of anonymous functions.
The JavaScript community needed a way to write asynchronous code that looked and behaved like synchronous code. We wanted the efficiency of non-blocking I/O with the readability of a simple top-to-bottom script. That is exactly why async/await was born: to provide a "syntactic sugar" coating over Promises.
ES6 certainly changed the landscape in some massive ways.
How Async Functions Work
At its core, an async function is a function that always returns a promise. Even if you return a simple string or a number, JavaScript automatically wraps that value in a resolved Promise.
To define one, you simply place the async keyword before the function declaration:
async function greet() {
return "Hello, World!";
}
greet().then(console.log); // Output: Hello, World!
The beauty of the async keyword is that it prepares the function to handle "pauses." It signals to the JavaScript engine that this function might contain an await expression, allowing the execution to continue via the main thread without blocking the rest of your application.
Here, in the diagram below the solid line shows blocking code while dashed shows the non-blocking one.
The Await Keyword
If async is the setup, await is the star of the show. The await keyword can only be used inside an async function. When you "await" a promise, the execution of the function is paused until that promise resolves.
Imagine you're ordering a coffee. Instead of standing at the counter frozen (synchronous) or walking away and hoping someone yells your name (callback), await lets you sit down and wait specifically for that coffee to be ready before you take the next step of drinking it.
async function getCoffee() {
console.log("Ordering...");
// The code "pauses" here until the promise resolves
const result = await brewCoffee();
console.log("Coffee is ready: " + result);
}
Crucially, await does not block the entire program. While the function is paused, the JavaScript engine is free to go do other things—like handling user clicks or animations. Once the promise is fulfilled, the engine jumps back into the function right where it left off.
Error Handling: Back to Basics
One of the biggest frustrations with raw Promises was error handling. You had to attach a .catch() at the end of every chain, and if you had nested promises, tracking where an error originated was a nightmare.
With async/await, we go back to the classic try...catch block. This is arguably the biggest win for readability. It allows you to handle both synchronous and asynchronous errors in the same place.
For me personally, I didn't mind the entire .then() or .catch() chaining, but maybe it is just due to the teachers and resources I learnt from, yet in most online discussions I've seen this being a problem so I am including this point here .
async function fetchData() {
try {
const data = await api.get("/user");
console.log(data);
} catch (error) {
console.error("Oops! Something went wrong:", error.message);//Btw, you should refrain from 'something went wrong' message.
}
}
This structure is intuitive. It makes asynchronous error handling look exactly like the error handling you use for basic logic, reducing the mental burden of understanding and debugging alike.
Comparison: Async/Await vs. Promises
It’s important to remember that async/await is built on top of Promises. It isn't a replacement; it’s a different interface. A table shall help you understand and summarize it better. For interviews and exams.
| Feature | Promises (.then) | Async/Await |
|---|---|---|
| Readability | Can become cluttered with chains | Clean, top-down flow |
| Error Handling | .catch() methods |
try...catch blocks |
| Conditionals | Harder to manage inside chains | Feels like standard logic |
| Debugging | Difficult to set breakpoints in chains | Easy to step through line by line |
While Promises are still great for running multiple tasks in parallel (using Promise.all), async/await is the clear winner for sequential logic.
The difference becomes more and more clear as we progress on the developer journey. The Async/Await feels intuitive but as I have been taught, it's never about which one's better, rather about what is the need for.
Conclusion
I hope you learnt something today. I wish you a wonderful sleep tonight, everybody needs it.
Remember more than one being better than another it's a matter of the use cases.
So dabble with the code, understand it better, and build something cool. Cheerios!


