Async JavaScript
Why your code doesn't always run top to bottom

JavaScript reads your code the same way you read a book: line one, line two, line three, in order. When it hits line five, lines one through four are already done. This is called synchronous execution. Each task finishes before the next one starts.
Most of the time, that's fine. Adding numbers, updating a variable, looping through a list: these things happen in microseconds. Nothing needs to wait for anything.
// Each line runs and finishes before the next one starts
console.log("Step 1: make tea");
console.log("Step 2: add milk");
console.log("Step 3: drink it");
// Output, every time, in this exact order:
// Step 1: make tea
// Step 2: add milk
// Step 3: drink it
The problem shows up when one of those steps takes time. Fetching data from a server. Reading a file. Waiting on a timer. Suddenly "finish before moving on" becomes a real problem.
What Blocking Feels Like
JavaScript runs in one thread, like One lane on the highway. When something stops in that lane, everything behind it stops too. There's no overtaking.
Here's a fake slow operation that shows the problem clearly. The slowTask() function below ties up JavaScript for three full seconds. Nothing else happens during that time, no button clicks, no animations, no anything.
// blocking code
function slowTask() {
// Spin the CPU for 3 seconds. Nothing else runs during this.
const end = Date.now() + 3000;
while (Date.now() < end) {}
}
console.log("Starting...");
slowTask(); // browser freezes for 3 seconds
console.log("Done."); // only prints after the freeze
A real network request works the same way if you wait synchronously. The difference is a spin loop wastes CPU. A network call mostly just sits there doing nothing while your app freezes. Either way, your user is staring at a dead screen.
This is why browsers started giving JavaScript a way to say "go do this, and tell me when it's done, but don't stop everything while you wait." That's asynchronous code.
Asynchronous Means Not to Wait
When you make something asynchronous, you're telling JavaScript to kick it off and move on. The rest of your code keeps running. When the slow thing finishes, it comes back and delivers its result.
The classic real-world version: you order food at a restaurant. The waiter takes your order, goes to the kitchen, and comes back later. You don't sit frozen at the table until your food is cooked. You talk to your friends. You look at your phone. The kitchen is doing its thing in the background.
console.log("Placed the order"); // runs immediately
setTimeout(() => {
console.log("Food is ready!"); // runs after 2 seconds
}, 2000);
console.log("Talking to friends"); // runs immediately, doesn't wait
// Output:
// Placed the order
// Talking to friends
// Food is ready! ( arrives 2 seconds later )
"Talking to friends" printed before "Food is ready!" even though it's written after the setTimeout. That's asynchronous behavior. JavaScript didn't wait at the timer. It moved on, and the timer delivered its result later when it was done.
The Event Loop
There's a piece of JavaScript's internals called the event loop. It's not something you write. It just runs constantly in the background, checking one thing: "is the main thread free? Is there anything waiting to run?"
When you kick off a timer, an API call, or a file read, JavaScript hands it off to the browser (or Node.js) and moves on. When that thing finishes, the result gets placed in a queue. The event loop watches that queue. The moment the main thread goes idle, the event loop pulls the next item out of the queue and runs it.
This is how JavaScript stays responsive even though it only has one thread. It doesn't actually do two things simultaneously. It does things quickly enough, and defers slow things, that it feels simultaneous.
Three Ways to Write Async Code
JavaScript has gone through a few different syntaxes for dealing with async code. You'll see all three in real codebases, so it's worth knowing what each one looks like.
Callbacks were first. You pass a function to be called when something finishes. They work, but nesting them for multiple sequential steps becomes a mess fast.
// old way
fetchUser(1, function(user) {
fetchPosts(user.id, function(posts) {
fetchComments(posts[0].id, function(comments) {
// three levels deep. now imagine five or six.
console.log(comments);
});
});
});
This staircase pattern is called callback hell. It's what production code actually looked like before Promises.
Promises cleaned this up. A promise is an object that represents a value that isn't ready yet. You chain .then() calls instead of nesting functions.
// Promise
fetchUser(1)
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.then(comments => console.log(comments))
.catch(err => console.error(err)); // one place to handle errors
async/await is the current standard. It's built on Promises, but it lets you write async code so it reads like synchronous code. The await keyword pauses the current function (not the whole thread) until the promise resolves.
// async-await
async function loadComments() {
try {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
console.log(comments);
} catch (err) {
console.error(err);
}
}
Same three network calls. Same asynchronous behavior. Reads almost like synchronous code.
Callback and Promises
Callbacks looks simple at first, but gets into callback hell
Promises has flat chains, and cleaner error handling
Still reads like async code, harder to follow when promise chaining gets involved
async / await
Syntactic sugar of Promises
Reads top-to-bottom, like synchronous code
try/catch handles errors the normal way
Fetching a User Profile
Here's what the async pattern looks like when something actually goes wrong. User1 loads the page. The app fetches their profile from an API. While that's happening, the rest of the UI stays live. When the data comes back, the card renders. If the server returns an error, the UI handles it cleanly.
// users.js
export async function fetchUserProfile(userId) {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error(Status ${res.status});
return res.json();
}
// profile.js
import { fetchUserProfile } from '../api/users.js';
async function renderProfile(userId) {
const card = document.getElementById("profile");
card.innerHTML = "Loading...";
try {
const user = await fetchUserProfile(userId);
card.innerHTML = `
<h2>${user.name}</h2>
<p>${user.email}</p>
`;
} catch (err) {
card.innerHTML = "Could not load profile.";
console.error(err);
}
}
renderProfile(42);
The "Loading..." line on line four is the important part. It runs synchronously, before the await. The user sees something immediately. The network call happens in the background.
What You Actually Get From Async Code
REFERENCES:




