JavaScript Async/Await Explained: From Callbacks to Clean Code
Introduction
JavaScript often has to wait — for a network request, a file, or a timer. The old way to handle waiting was nested callbacks, which quickly became hard to read. Promises improved that. Async/await makes it read almost like normal, top-to-bottom code.
If you understand promises and one keyword pair, async and await, you can write most asynchronous JavaScript you will need day to day.
The basic pattern
A function marked async always returns a promise. Inside it, await pauses that function until a promise settles, without blocking the rest of the program.
async function getUser(userId) {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const user = await response.json();
return user;
}
getUser(1).then((user) => console.log(user.name));
Read it like normal code: fetch the user, wait for the JSON, return the user. The await lines are where the waiting happens, and they are easy to spot.
Handling errors with try/catch
Without error handling, a failed request becomes an unhandled promise rejection. Wrap awaited calls in try/catch, just like synchronous code.
async function getUserSafely(userId) {
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Could not load user:", error.message);
return null;
}
}
Two habits matter here. First, check response.ok, because fetch only rejects on a network failure, not on a 404 or 500. Second, return a safe fallback, like null, so the caller can handle the missing data cleanly.
Sequential vs parallel
Awaiting in a loop runs tasks one after another. That is correct when one task depends on the previous one, but slow when the tasks are independent.
// Sequential: second request waits for the first
const user1 = await getUser(1);
const user2 = await getUser(2);
// Parallel: both start at once, then wait for both
const [userA, userB] = await Promise.all([getUser(1), getUser(2)]);
Use Promise.all when the tasks do not depend on each other. Note that if any promise in Promise.all rejects, the whole thing rejects, so handle errors for the group, not just for one item.
Common beginner mistakes
- Forgetting
await, then wondering why a variable holds a promise instead of data. - Using
awaitinside a normal function instead of anasyncfunction. - Awaiting inside a loop when the tasks are independent — use
Promise.allinstead. - Forgetting error handling, so one failed request crashes the flow silently.
Takeaway
Mark the function async, await the promises you need, handle errors with try/catch, and use Promise.all for independent work. That covers most real async code, without callback nesting.
Comments
Post a Comment