The IRCTC Tatkal Problem
At 10:00 AM sharp, you click "Book Now" on IRCTC for a Tatkal ticket. The page shows a small spinner and the text "Checking seat availability...". Here is the part worth noticing: while that spinner spins, you can still scroll the page, click the Cancel button, or switch to another browser tab. The page has not frozen, even though it is clearly still waiting for an answer from IRCTC's server, which might take one second or five.
This is strange when you remember a basic fact about JavaScript: it runs on a single thread. One thread means one instruction executes at a time, in order, like a single clerk at a counter who can only serve one customer before moving to the next. If that clerk is busy, everyone else waits. So how does IRCTC's page stay clickable while it is genuinely still waiting for data that has not arrived yet? Answering that question precisely is what this chapter is about, and async/await is the modern syntax JavaScript gives you to write that kind of code clearly.
What Blocking Actually Looks Like
Before seeing the solution, you need to see the problem in real code. Here is a function that "waits" for 3000 milliseconds the wrong way — by simply keeping the CPU busy in a loop until the time is up:
function blockFor(ms) {
const start = Date.now();
while (Date.now() - start < ms) {
// do nothing, just keep checking the clock
}
}
console.log("Before");
blockFor(3000);
console.log("After");
Trace this line by line. Line 1 prints "Before" immediately. Line 2 calls blockFor(3000), and because JavaScript has only one thread, that thread is now trapped inside the while loop, checking the clock over and over, for a full 3000 milliseconds. During those three seconds, nothing else can happen — not a click, not a scroll, not a keypress, not even a screen repaint — because the single thread that would normally respond to those events is busy spinning in the loop. Only after 3000ms passes does the loop exit, and "After" prints. This is called blocking (or synchronous) code: each line must fully finish before the next line can even begin, and if a line takes a long time, everything else waits with it. A real IRCTC page written this way would freeze solid every time it asked the server for seat data.
The Trick: Handing the Wait to Someone Else
Real network requests do take time — often much more than 3 seconds on a slow mobile network — yet real websites don't freeze. The trick is that JavaScript never actually sits and waits itself. When you start a network request or a timer, JavaScript hands that waiting job off to the browser (or, in a Node.js server, to the operating system), and immediately moves on to the next line of your program. Only when the browser finishes the job — the timer runs out, or the server's response arrives — does it hand control back to JavaScript to continue. Compare blockFor with JavaScript's built-in setTimeout, which does exactly this handoff:
console.log("Before");
setTimeout(function () {
console.log("Seats found!");
}, 3000);
console.log("After");
Trace it again. Line 1 prints "Before". Line 2 calls setTimeout, which registers "run this function after 3000ms" with the browser's own timer system, and then setTimeout itself returns instantly — it does not wait. So JavaScript immediately moves to line 6 and prints "After". The actual console output order is:
Before
After
Seats found! (this one appears about 3 seconds later)
Notice "After" printed before "Seats found!", even though setTimeout was written before console.log("After") in the source code. The callback function only runs once 3000ms have passed and JavaScript has finished everything else it was doing. This is asynchronous (non-blocking) behaviour: a slow operation is started, the rest of the program keeps running without waiting for it, and a piece of code runs later to handle the result when it's ready.
The Old Way: Callbacks, and Why They Get Messy
The function you pass to setTimeout to run later is called a callback. Callbacks work, but chain a few of them together and the code becomes hard to read. Imagine booking a ticket requires three steps in sequence: log in, then check seat availability, then confirm payment — and each step is asynchronous because each talks to a server:
login(userId, function (user) {
checkAvailability(user, function (seats) {
confirmPayment(user, seats, function (receipt) {
console.log("Booked:", receipt);
});
});
});
Each step is nested one level deeper inside the previous one's callback, because each step can only start once the previous one's data has arrived. Programmers call this shape callback hell or the "pyramid of doom" — the indentation marches to the right with every new step, and adding error handling to each level (what if login fails? what if no seats are left?) makes it worse still. JavaScript needed a cleaner way to say "do this, then this, then this, waiting for each one" without nesting.
Promises: A for a Future Value
A Promise is an object that represents a value which doesn't exist yet but will exist eventually — the result of an asynchronous operation. Think of the numbered token you get at a busy Irani cafe counter: it isn't your food, but it's a guarantee that your order is being prepared and the token will eventually be exchanged for a plate (or, if the kitchen runs out of an ingredient, for an apology). A Promise is always in exactly one of three states:
- pending — still waiting, no result yet (the kitchen is cooking)
- fulfilled — finished successfully, with a value (your plate arrives)
- rejected — finished with an error (kitchen is out of stock)
Rewriting setTimeout to return a Promise instead of taking a callback looks like this:
function checkAvailability(trainNo) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
const seatsLeft = 4;
if (seatsLeft > 0) {
resolve(seatsLeft); // fulfilled: hand back the value
} else {
reject(new Error("No seats left")); // rejected: hand back an error
}
}, 2000);
});
}
resolve and reject are two functions the Promise gives you; calling resolve(value) moves the Promise to fulfilled with that value, calling reject(error) moves it to rejected. To use the result once it's ready, you attach a .then() handler:
checkAvailability("12951")
.then(function (seats) {
console.log("Seats available:", seats);
})
.catch(function (err) {
console.log("Error:", err.message);
});
This already reads better than nested callbacks — each .then() chains off the previous one instead of nesting inside it — but with three or four chained steps, a long column of .then(function...) blocks still gets visually noisy. async/await is syntax built directly on top of Promises that removes that noise entirely.
Async/Await: Writing Asynchronous Code That Reads Like Synchronous Code
Two new keywords make this possible. Writing async before a function definition marks that function as asynchronous, and inside an async function you may use await before any expression that produces a Promise. await pauses that function at that exact line until the Promise settles (fulfills or rejects), then continues with the resulting value as if it were an ordinary return value. Here is the same seat-check, rewritten:
async function bookTicket(trainNo) {
const seats = await checkAvailability(trainNo);
console.log("Seats available:", seats);
}
bookTicket("12951");
Read the body of bookTicket top to bottom: it looks exactly like ordinary, synchronous code — call a function, get a value back, use it on the next line. There is no callback, no .then(). The await keyword is what makes this possible: it tells JavaScript "pause this function here (and only this function) until checkAvailability's Promise settles, then hand me the resolved value and carry on." The three-step booking chain that needed nested callbacks earlier becomes:
async function bookTicket(userId, trainNo) {
const user = await login(userId);
const seats = await checkAvailability(trainNo);
const receipt = await confirmPayment(user, seats);
console.log("Booked:", receipt);
}
Four lines, no nesting, no pyramid, and each step's data is available by name on the next line — exactly like ordinary synchronous code, even though every one of those three calls involves real waiting for a server.
Two Facts About Async Functions Worth Memorizing
First: an async function always returns a Promise, even if the code inside it looks like it returns a plain value. If bookTicket above had a return receipt; line, calling bookTicket(...) from outside would not give you the receipt directly — it would give you a Promise that eventually resolves to the receipt, which you'd still need to await or .then() to unwrap. Second: await is only legal inside an async function (with one narrow exception, top-level await in module files, which is beyond this chapter) — you cannot sprinkle await into ordinary functions.
Handling Failure: try/catch
A rejected Promise inside an await throws an ordinary JavaScript error at that line, which means you catch it with a regular try/catch block — no special async syntax needed for error handling:
async function payWithUPI(amount, balance) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
if (amount <= balance) {
resolve("Payment of Rs " + amount + " successful");
} else {
reject(new Error("Insufficient balance"));
}
}, 1000);
});
}
async function checkout() {
try {
const result = await payWithUPI(500, 300);
console.log(result);
} catch (error) {
console.log("Payment failed:", error.message);
}
}
checkout();
Trace it: checkout() is called, which calls payWithUPI(500, 300). Inside that function, after 1000ms, the check 500 <= 300 is false, so reject(new Error("Insufficient balance")) runs. Back in checkout, the await on that rejected Promise throws the error at that exact line, which the surrounding try catches, jumping straight to the catch block. After a 1-second delay, the console prints exactly one line: Payment failed: Insufficient balance. If you had raised balance to 800, resolve(...) would fire instead, await would simply return the success string, and the catch block would never run at all.
A Costly Mistake: Awaiting One at a Time When You Don't Need To
Suppose booking a ticket needs three independent pieces of information — seat availability, current fare, and recent reviews of the train — and each one is a separate API call that takes about 1000ms. It's tempting to write:
async function loadTicketPage() {
const seats = await checkAvailability(); // waits ~1000ms
const fare = await getFare(); // then waits ~1000ms more
const reviews = await getReviews(); // then waits ~1000ms more
return { seats, fare, reviews };
}
This works, but do the arithmetic: each await pauses loadTicketPage until that specific call finishes before starting the next one, so the three waits stack up: 1000 + 1000 + 1000 = 3000ms total, even though none of these three calls actually depends on the others' results. Since they're independent, you can start all three at once and let them run concurrently, then wait for all of them together, using Promise.all:
async function loadTicketPage() {
const [seats, fare, reviews] = await Promise.all([
checkAvailability(),
getFare(),
getReviews(),
]);
return { seats, fare, reviews };
}
Here all three calls are started in the same instant (none of them has an await in front of it individually, so none blocks the next from starting), and Promise.all waits only for the slowest one to finish. Total time is now roughly max(1000, 1000, 1000) = 1000ms — three times faster than the sequential version, for the exact same three network calls. The rule: only write await one after another, in sequence, when a later step genuinely needs an earlier step's result (like needing your user object before you can call checkAvailability for that user); when steps are independent, start them together.
How JavaScript Actually Schedules the Resumption
The diagram below shows what happens on the timeline when your code calls an async function that awaits a network wait, while the rest of the program keeps running.
The yellow boxes are the single async function pausing and resuming; the green box is other code in your program (which is not inside that async function) running in between, completely unaffected. The red box is work happening outside JavaScript entirely — in the browser's networking layer — which is precisely why the main thread is free to run that green box while it waits.
Predicting Execution Order: A Trickier Example
Now trace a harder case, mixing a timer with an async function, to see the ordering rule precisely:
console.log("1");
setTimeout(function () {
console.log("2");
}, 0);
async function foo() {
console.log("3");
await null;
console.log("4");
}
foo();
console.log("5");
Work through it step by step. console.log("1") runs first — obviously, it's the first line: prints 1. Next, setTimeout(..., 0) is called; even with a delay of 0ms, setTimeout always hands its callback to the browser's timer queue and returns immediately — it never runs its callback right away. So JavaScript moves on without printing anything yet. Next, foo() is called. Since foo is an async function, everything inside it runs completely normally, synchronously, right up until the first await — so console.log("3") runs immediately: prints 3. Then await null is reached, and foo pauses, scheduling its remainder (console.log("4")) to run as soon as possible after the current script finishes, but the current script hasn't finished yet: control returns to the line right after foo() was called. That line is console.log("5"), which runs: prints 5. Only now has every line of the original, top-level script finished running. JavaScript checks what's waiting: foo's paused continuation (from the await) is queued ahead of the setTimeout callback, because promise continuations are always given priority over timer callbacks, even a 0ms one. So console.log("4") runs next: prints 4. Finally, with nothing else pending, the timer callback runs: prints 2. Full output order:
1
3
5
4
2
The rule to take away: all synchronous code in the currently running script finishes first, top to bottom, regardless of where await or setTimeout calls appear inside it. Once that's done, JavaScript resumes paused awaits before it runs pending setTimeout callbacks, even if the timer's delay was zero.
Correcting a Common Misconception
Many students, on first meeting await, assume it means "freeze everything until this finishes" — treating it as a fancier version of the blockFor busy-wait from the start of this chapter. This is incorrect, and the difference matters. await only pauses the single async function it's written inside. It does not block the call stack, does not block the browser's UI, and does not stop any other code — including code outside that function, other event handlers, or other async functions running elsewhere — from executing while the wait continues. That is exactly why the IRCTC page from the start of this chapter stays clickable while "Checking availability..." is on screen: the function checking availability is paused on its own await, but nothing else in the page is paused with it. Contrast this precisely with blockFor(3000), which occupied the single thread directly with a loop and froze absolutely everything, because a busy loop gives the thread no opportunity to do anything else, while await deliberately hands the thread back so it can.
A second, smaller misconception worth naming: async/await is not a different, faster execution mechanism than Promises — it is the same Promise machinery underneath, just written with keywords instead of .then() chains. There is no performance difference between the .then() version of checkAvailability shown earlier and its await equivalent; both run at the same speed, because both are, at their core, exactly the same Promise being resolved on exactly the same schedule.
Summary
- JavaScript runs on a single thread, so a genuinely blocking operation (like a busy-wait loop) freezes everything until it finishes.
- Slow operations — network calls, timers, file reads — are handled asynchronously: JavaScript hands the waiting off (to the browser or OS) and keeps running other code, resuming only when the result is ready.
- A Promise is an object representing a value that will exist later; it is pending, then becomes fulfilled (with a value) or rejected (with an error).
asyncmarks a function as returning a Promise automatically; inside it,awaitpauses that function — and only that function — until a Promise settles, then unwraps its value.- A rejected, awaited Promise throws at that line, so ordinary
try/catchhandles async errors — no separate syntax needed. - Chain
awaits in sequence only when each step needs the previous step's result; for independent async calls, start them together and usePromise.allto wait for all of them concurrently, which can be several times faster. - Within one script, all synchronous code finishes first; then paused
awaitcontinuations run before pendingsetTimeoutcallbacks, even a 0ms one.
Check Your Understanding
- Predict the exact console output, in order, for:
console.log("A"); async function run() { console.log("B"); await Promise.resolve(); console.log("C"); } run(); console.log("D"); - The function
getFare()andgetSeats()each take about 1500ms and do not depend on each other's results. Rewrite this sequential code so both requests run concurrently, and state the new approximate total time:async function load() { const fare = await getFare(); const seats = await getSeats(); return { fare, seats }; } - A classmate writes:
function pay() { const r = await charge(500); return r; }and it fails to run. Name the exact rule this code breaks, and fix it. - True or false, with a one-line justification: "
awaitstops the entire web page from responding until the awaited Promise settles." - An
asyncfunction has no explicitreturnstatement at all. What does calling it produce —undefined, or something else? Explain using what you know about whatasyncfunctions always return.
Practice Exercises
Now it is time to practice! Complete these challenges to solidify your understanding:
- Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
- Exercise 2: Find a real-world example where async/await: writing asynchronous code is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
- Exercise 3: Create a mind-map connecting async/await: writing asynchronous code to at least 3 other topics you have studied.