Introduction
If JavaScript stopped and waited for each network request, the whole page would freeze until the answer arrived. Instead, it starts slow work and carries on. This makes programs responsive, but the order in which things happen is no longer the order of the lines. This lesson gives you the exact rules, so that the order stops being a mystery.
How it works
JavaScript runs on one thread: it does one thing at a time. Slow work (a timer, a network request, a file) is handed to the browser or Node.js, and JavaScript carries on. When that work finishes, a callback is put in a queue, and the event loop runs it once the current code has finished.
The output order surprises everyone the first time:
- All the ordinary (synchronous) code runs first, to the end.
- Then every microtask: the callbacks of promises that have settled.
- Then the tasks (also called macrotasks): timers, clicks, network events, one at a time, each followed by any new microtasks.
So even setTimeout(fn, 0) runs after the promise callbacks. The visualizer marks each turn of the event loop.
Promises. A promise is an object for a value that is not ready yet. It is pending, then either fulfilled with a value or rejected with an error. .then(fn) runs fn with the value, and .catch(fn) runs fn with the error. Each returns a new promise, so they chain.
async and await. An async function always returns a promise. Inside it, await promise pauses that function (not the whole program) until the promise settles, then gives its value, or throws its error so that an ordinary try/catch handles it:
// async functions and await, with timers standing in for slow work.
function wait(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
async function loadUser() {
console.log("Loading...");
const user = await wait(300, { name: "Mitu" });
const posts = await wait(200, ["Hello", "Week 2"]);
return `${user.name} has ${posts.length} posts`;
}
async function main() {
const summary = await loadUser();
console.log(summary);
const both = await Promise.all([wait(100, "a"), wait(50, "b")]);
console.log(both);
try {
await Promise.reject(new Error("server is down"));
} catch (err) {
console.log("Caught:", err.message);
}
}
main();
console.log("main has started");Promise.all([p1, p2]) waits for several promises at once and gives an array of their values. It takes only as long as the slowest one, which is why it beats awaiting each job in turn.
fetch. In a browser, fetch(url) asks a server for data and returns a promise of the response. StepVioz programs run without a network, so here fetch always rejects, and the examples use timers to stand in for slow work. A later lesson covers fetching in detail.
Syntax
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function main() {
try {
await wait(100);
console.log("done");
} catch (err) {
console.log("failed:", err.message);
}
}
main();
Example
// Which runs first: timers, promises or ordinary code?
console.log("1: start");
setTimeout(() => console.log("4: timer"), 0);
Promise.resolve().then(() => console.log("3: promise"));
console.log("2: end of the script");Line by line
"1: start"is printed immediately.setTimeoutschedules a task and returns at once. Nothing is printed.Promise.resolve().then(...)schedules a microtask. Nothing is printed."2: end of the script"is printed, and the script is finished.- The microtask queue is emptied, which prints the promise line.
- The timer's task runs last, even though its delay was 0.
Expected result
1: start
2: end of the script
3: promise
4: timer
In the real world
Loading a page's data, saving a form and uploading a photo are all asynchronous. Code that makes several requests often uses Promise.all so that a dashboard loads in the time of its slowest request, rather than the total of all of them.
Common mistakes
- Forgetting
await. You get the promise itself, not its value. - Using
awaitoutside an async function. In an ordinary script that is a SyntaxError. Put the code in an async function and call it. (Only JavaScript modules allowawaitat the top level.) - Expecting
setTimeout(fn, 0)to run before the code after it. - A promise chain with no
.catch, or an await with no try. A failure becomes an unhandled rejection. - Awaiting in a loop one by one when the jobs could run together with
Promise.all.
Best practices
- Prefer
asyncandawaitto long.thenchains, because they read from top to bottom. - Handle errors at the level that can tell the user something useful.
- Use
Promise.allfor independent jobs, and sequentialawaitonly when one step needs the last.
Summary
JavaScript does one thing at a time, and the event loop runs callbacks when it is free: first synchronous code, then microtasks, then tasks. Promises represent future values. async and await let you write asynchronous steps almost like ordinary code.
Quick check
Key takeaways
- Synchronous code always finishes first, then microtasks, then tasks.
- await pauses only its own function, and the rest of the program carries on.
- Handle failures with try/catch around await, or with .catch.
Practice problems for this lesson
My notes
Write down what you want to remember from this lesson. Notes are saved on this device as you type, and appear on your dashboard.