Asynchronous JavaScript
By the end of this lesson
Use promises and async/await without blocking the page.
A browser tab runs your JavaScript on one thread. One thing at a time, in order. Clicks, key presses, timers and arriving network responses do not interrupt — they wait in a queue until your current work finishes.
So any code that takes a while is a problem. While it runs, nothing else can: no clicks handled, no scrolling, no repainting. The page is frozen, and the reader's only feedback is that it stopped responding.
Asynchronous code is how you wait for something without holding the thread. This lesson assumes you are comfortable with functions and objects from the JavaScript module.
// loadExpenses returns a promise, so calling it does not block
const pending = loadExpenses();
console.log(pending); // a Promise object, not the expenses
console.log("this line runs immediately");
// Handle the result when it arrives
pending
.then((expenses) => console.log(expenses.length))
.catch((error) => console.error("Could not load expenses:", error));- A promise represents a result that is not ready. Calling the function starts the work and hands you the receipt straight away.
- The console.log on line 3 prints a Promise object. This is what you see when a promise is used where its value was expected — an object rather than your data.
- then registers what to do when the value arrives. catch registers what to do if it fails.
- Line 4 runs before either of those. That is the point: the thread was free to carry on.
- This style works, and nesting it a few levels deep becomes hard to follow. await, next, is the same mechanism with better-shaped code.
async function showExpenses(employeeId) {
setLoading(true);
try {
const expenses = await loadExpenses(employeeId);
renderExpenses(expenses);
} catch (error) {
showError("Could not load expenses. Try again.");
console.error(error);
} finally {
setLoading(false);
}
}- async on a function means it returns a promise and is allowed to use await inside.
- await pauses this function until the promise settles, then gives you the value. Only this function pauses — the thread is released, so the page stays responsive.
- The code now reads top to bottom in the order things happen, which is the main reason to prefer it.
- A rejected promise becomes a thrown error at the await, so ordinary try/catch works. That is the second reason: one error mechanism rather than two.
- finally runs whether it succeeded or failed, which is exactly where the loading indicator should be turned off. Put it in both branches instead and one path eventually forgets.
- Calling showExpenses returns a promise immediately. The caller decides whether to await it, and if it does not, it should still handle failure.
The vocabulary, so error messages make sense:
- Pending
- Started, no result yet. This is what a promise is when you first receive it.
- Fulfilled
- Finished with a value. await hands you that value.
- Rejected
- Finished with a reason, usually an Error. await throws it.
- Settled
- Fulfilled or rejected. A promise settles once and then never changes, so the result can be read later.
- await
- Waits for a promise to settle. Only usable inside an async function, or at the top level of a module.
- Promise.all([...])
- One promise for several. Fulfils with an array of results when all are done, and rejects as soon as any one fails.
- Promise.allSettled([...])
- Waits for every promise regardless of failures and reports the outcome of each. Right when a partial result is still useful.
// Sequential: the second request starts after the first finishes
async function loadPageSlowly(employeeId) {
const employee = await loadEmployee(employeeId);
const expenses = await loadExpenses(employeeId);
return { employee, expenses };
}
// Parallel: both start now, and we wait for the slower one
async function loadPage(employeeId) {
const [employee, expenses] = await Promise.all([
loadEmployee(employeeId),
loadExpenses(employeeId),
]);
return { employee, expenses };
}- In the first version each await is a full stop. If both requests take 300ms, the function takes about 600ms.
- In the second, both functions are called before anything is awaited, so both requests are in flight together. The total is about 300ms — the slower of the two.
- Promise.all is the right shape only when the requests are independent. If the second needs the employee's department from the first, sequential is not a mistake, it is a requirement.
- Promise.all rejects the moment any promise rejects, and you lose the successful results. When the page can still be useful with one of the two, Promise.allSettled and a per-section error is the better trade.
- Destructuring the array keeps the results in a readable order. That order matches the order you passed them in, not the order they finished.
Summary
- One thread runs your code, so long synchronous work freezes the page
- A promise is a result that has not arrived; await gives you the value without holding the thread
- Rejections surface as thrown errors at the await, so try/catch/finally works normally
- Promise.all runs independent requests together; allSettled keeps partial results when one fails
- async removes blocking, not work — heavy computation needs less work, chunking, or a worker
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Load two things at once
Write a function that loads an employee and their expenses, renders both, shows a loading state while it works and an error message if either fails. Make the two requests run at the same time.
Then change it so that if the expenses fail but the employee loads, the page still shows the employee with an error in the expenses panel only.
Show solution
Promise.all gives the parallel version and one failure path. It is the right default: less code, and a single error state.
The second version needs Promise.allSettled, because all rejects as soon as anything fails and throws away results you wanted. Each result then carries its own status, and each panel renders independently.
This is a real trade-off rather than an improvement. allSettled is more code and forces you to think about partial states. For a page where one panel is genuinely useful without the other it is worth it. For a page that is meaningless without both, the simpler version is better.
// One error state, simplest to reason about
async function loadEmployeePage(employeeId) {
setLoading(true);
try {
const [employee, expenses] = await Promise.all([
loadEmployee(employeeId),
loadExpenses(employeeId),
]);
renderEmployee(employee);
renderExpenses(expenses);
} catch (error) {
showPageError("Could not load this employee.");
console.error(error);
} finally {
setLoading(false);
}
}
// Partial success: each panel succeeds or fails on its own
async function loadEmployeePagePartially(employeeId) {
setLoading(true);
const [employeeResult, expensesResult] = await Promise.allSettled([
loadEmployee(employeeId),
loadExpenses(employeeId),
]);
if (employeeResult.status === "fulfilled") {
renderEmployee(employeeResult.value);
} else {
showPageError("Could not load this employee.");
}
if (expensesResult.status === "fulfilled") {
renderExpenses(expensesResult.value);
} else {
showExpensesError("Expenses are unavailable right now.");
}
setLoading(false);
}Think about it
Think about it
A colleague reports that an expense report page freezes for three seconds, and suggests making the calculation async to fix it. The calculation loops over 200,000 rows in memory. Will async help?
Show solution
No. async does not create a second thread. The loop is one long task on the main thread either way, and wrapping it in an async function only changes when that task starts.
Async solves waiting, not working. A network request is idle time that can be given back; a loop is work that has to happen somewhere.
Real options: do less work — calculate a total on the server, or only for the rows on screen. Break the loop into chunks that yield between them, so the page can repaint and respond. Or move the whole calculation to a web worker, which is a genuinely separate thread. The first is usually the cheapest and the most effective.
Knowledge check
Nothing is recorded and there is no score. The explanation appears either way.
Saved in this browser only.