Synchronous vs asynchronous
Learn why blocking JavaScript freezes the page, how browser and Node host APIs wait outside the call stack, and why callbacks run later without interrupting current code.
- 01Why long work freezes the pageExplain how a long synchronous job keeps the main thread busy, so the page can't draw or react to clicks.
- 02What the host does for JavaScriptDescribe timers, server requests, and file reads as jobs the host waits for, outside JavaScript.
- 03Why some callbacks run laterPredict the order of printed lines, using the rule that running code is never cut into.
Now or later: the main idea
JavaScript runs your code one line at a time. Most lines finish at once, and then the next line runs. But some jobs take time, like waiting for a timer or for data from a server. This lesson is about one question: does your code wait for the job right now, or does it keep going and deal with the result later?
Two words name the two ways:
- Synchronous code ("sync" for short) runs now. The next line waits until it is done.
- Asynchronous code ("async" for short) starts a job now and deals with the result later. The next line does not wait.
In a browser, your JavaScript runs on the main thread. The main thread is the one worker that runs your code. The browser uses the same worker to draw the page and to handle clicks. So if your code keeps the main thread busy, the page can't do anything else.
You met the host in Engines, runtimes & hosts. The host is the program that runs JavaScript, such as a browser or Node. You saw how the host lines up waiting work in The JavaScript runtime at a glance. This lesson uses both ideas with everyday code: timers, click handlers, and callbacks. A callback is a function you pass to other code, so that code can call it. You met callbacks in Function expressions & callbacks.
You call a bakery to ask if your cake is ready. You stay on the line and wait until they answer. You can't do anything else. That is synchronous waiting.
Now you send the bakery a text instead. You put your phone away and do other things. When the reply arrives, you read it. That is asynchronous waiting.
- In real life: On a phone call, you stay on the line and wait
- In JavaScript: Sync code: the next line waits
- In real life: You send a text and get on with your day
- In JavaScript: Async code: the host waits, and JavaScript runs the next lines
- In real life: The reply arrives later, and you read it then
- In JavaScript: The callback runs later
Synchronous code runs now, from start to end, before anything else runs. Asynchronous code starts a job now. The host does the waiting. Your callback runs later, when the job is done.
Blocking the main thread
INTERACTIVEBlocking means JavaScript keeps the main thread so busy that the browser can't do its own jobs. While the main thread is blocked, the browser can't:
- draw changes on the page,
- scroll smoothly,
- respond to clicks or key presses.
Any long job in JavaScript does this. A long calculation blocks. So does a loop that does nothing but wait. So does JSON.parse on a huge piece of text. (JSON.parse turns JSON text into a JavaScript value.) In each case, JavaScript is busy now, so everything else waits.
Try it below. The kitchen experiment has two ways to "bake":
- The synchronous bake runs a loop for just under a second. The loop keeps checking the clock until the time is up.
- The asynchronous bake asks the host's timer to wait instead. JavaScript stays free.
Watch the counter. It tries to go up ten times a second. The loop is kept short on purpose, so it can't freeze the page for long.
function bakeSynchronously() { const started = performance.now(); while (performance.now() - started < 1000) { // JavaScript is busy here, so the page can't update. } console.log("sync bake done");} function bakeAsynchronously() { setTimeout(() => { console.log("async bake done"); }, 1000);}The counter tries to go up ten times a second. If the main thread is blocked, the number stops changing.
- The counter is running. Try the two bake buttons.
Run the synchronous bake. Then press Reset and run the asynchronous bake. Watch whether the counter moves.
During the synchronous bake, the counter stops. Even that small number can't be drawn again. The browser may notice your click or key press, but it can't run the code for it until JavaScript is done with the main thread. That is why real websites never wait with a loop like this.
console.log("work starts");
for (let i = 0; i < 3; i = i + 1) {
console.log("work " + i);
}
console.log("work ends");Line 1 prints work starts. The loop on lines 2 to 4 prints work 0, work 1, and work 2. Then line 5 prints work ends. Each line waits for the one before it. If the loop ran a million times, the page would wait for all of them.
The host does the waiting
INTERACTIVEThe JavaScript language itself has no timer, no web page, no files, and no network. The host adds them. A host API is a tool the host gives to JavaScript. Some examples:
setTimeoutruns a function after a delay, in browsers and in Node.fetchasks a server for data.- Node has tools that read files.
When you call a host API, JavaScript hands the job to the host. If you pass a callback, the host keeps it for later. It does not run it yet.
You are cooking. You ask your assistant to watch the oven and tell you when the cake is done. You keep chopping vegetables. When the cake is done, the assistant leaves a note on your desk. You read the note when you finish chopping.
- In real life: You ask your assistant to watch the oven
- In JavaScript: JavaScript calls a host API like
setTimeout - In real life: You keep chopping vegetables
- In JavaScript: The rest of your script keeps running
- In real life: The assistant leaves a note on your desk
- In JavaScript: The host lines up the callback to run
console.log("1. JS starts the recipe");setTimeout(() => { console.log("3. host puts the callback back");}, 1000);console.log("2. JS keeps cooking");Press Start. JavaScript will hand a timer to the host, and keep going while the host counts.
This is how async code keeps a page quick to react. The waiting happens in the host, not in JavaScript. A timer callback, a click handler, and a handler for server data all work the same way: the host hands them back later. Promises are another way to work with a result that comes later. You'll see them further down.
console.log("order placed");
setTimeout(() => console.log("tea ready"), 0);
console.log("next order");Line 1 prints order placed. Line 2 hands the timer and its callback to the host. JavaScript doesn't wait, so line 3 prints next order. After the script ends, the host hands the callback back, and it prints tea ready.
next order. Later the host hands the callback back, and it prints tea ready.The waiting happened in the host. The callback itself still runs as JavaScript, on the main thread.
Some callbacks run later
STEP THROUGHA callback is a function you pass to other code. That other code decides when to call it.
- Some callbacks run right away.
array.mapcalls its callback once for each item, whilemapis still running. - Other callbacks run later. A timer callback, an event listener, and a handler for server data all wait until the host has something to report. (An event listener is a function that runs when something happens, such as a click.)
The classic test is setTimeout(..., 0). The 0 is the delay in milliseconds. A delay of zero does not mean "run right now". It means "host, hand this callback back as soon as you can, after the current script ends".
The delay is 0, but the timer callback still runs after the whole script is done. So the order is A, C, B.
script
setTimeout(() => { console.log("B: timer callback");}, 0);console.log("C: end");B later, even with a delay of 0.setTimeout(callback, 0) still hands the callback to the host. The callback runs later, after the script. That is why A prints, then C, then B.
console.log("ready")setTimeout(() => console.log("ding"), 0)- a handler for a
fetchreply - the callback passed to
array.map button.addEventListener("click", handler)JSON.parse("{\"ok\":true}")
Sort each line of code. Does it run now, while the current script runs? Or later, after the host has something ready?
Being a function doesn't make a callback async. The code that receives the callback decides when to call it. Knowing this prevents many "why is this value not ready yet?" bugs.
Three ways to run code later
Callbacks, promises, and async/await all do the same thing: start a job now, then deal with its result later. None of them makes the job faster. None of them makes two pieces of JavaScript run at the same time.
function greetLater(callback) {
setTimeout(() => callback("Asha"), 0);
}
greetLater((user) => console.log("Hello, " + user));
console.log("message sent");greetLater receives a function called callback and gives it to the timer. So the call to greetLater on line 4 finishes before the callback runs. That is why line 5 prints message sent first, and then the callback prints Hello, Asha.
Promise.resolve("tea").then((drink) => {
console.log("ready: " + drink);
});
console.log("order placed");A promise is an object that stands for a result that may arrive later. Promise.resolve("tea") makes a promise that already holds "tea". then attaches a handler: a function that receives the result. Even though the result is already there, then still runs its handler later. That is why the output is order placed, then ready: tea.
async function makeOrder() {
const drink = await Promise.resolve("tea");
console.log("ready: " + drink);
}
makeOrder();
console.log("order placed");The async keyword makes a function return a promise. await pauses that one function until the promise has its result. It does not freeze the whole page. While makeOrder is paused, the rest of the script runs, so line 6 prints order placed first.
- Use a callback for one small "tell me when it's done".
- Use
thento chain promise steps one after another. - Use
async/awaitwhen you wait for several things in a row and want the code to read from top to bottom.
Run to completion: no cutting in
STEP THROUGHRun to completion means JavaScript finishes the code it is running before it starts anything else. A timer can be ready while a loop is running, but it can't cut in. The timer callback waits until the loop and the rest of the script are done.
This rule makes code easier to follow. While your function runs, no callback can jump in and change its variables halfway through.
The timer was ready almost at once. But JavaScript finished the whole script, loop included, before it ran the callback.
script
console.log("timer callback");}, 0);console.log("loop starts");for (let i = 0; i < 3; i = i + 1) { console.log("loop " + i);}console.log("loop ends");The table below sums up the two ways. Read each row as a question, then the answer for sync code and for async code.
| Question | Synchronous (now) | Asynchronous (later) |
|---|---|---|
| Who waits? | JavaScript waits, on the main thread. | The host waits, outside JavaScript. |
| Can the page react while it waits? | No. The page waits until JavaScript is done. | Usually yes. JavaScript is free until the callback runs. |
| When does the callback run? | There may be no callback. The work happens now. | Later, after the current script is done. |
| What are some examples? | console.log, JSON.parse, and array.map. | setTimeout, click handlers, and handlers for server data. |
This also explains the old browser pop-up boxes alert and prompt. While the box is open, the whole page waits. The Talking to the user lesson shows how to use them. For now, remember that they block the main thread, so use them with care.
Where you'll use this
The sync/async split shows up whenever code waits for something. Some examples:
- a search box that waits until you stop typing, then searches,
- a loading spinner that spins while data comes back from a server,
- a progress bar while a file uploads,
- a Save button that keeps working while your data is sent to a server.
- Do quick work now, synchronously.
- Hand real waiting to a host API.
- If the work itself is huge, split it into small pieces, or move it to a worker. (A worker runs JavaScript away from the main thread.) Then the main thread stays free for the page.
For example, checking one form field can happen now. Asking a server whether a username is free should be async. Drawing a small part of the page can happen now. Reading a giant reply or filtering a huge list may need splitting, so it doesn't block typing.
function showResults(results) {
console.log("showing " + results.length + " results");
}
setTimeout(() => showResults(["tea", "toast"]), 0);
console.log("searching...");Line 6 prints searching... right away. The timer on line 5 stands in for the wait for the server. When the timer is done, its callback passes two items to showResults, which prints showing 2 results. A real app would show the results on the page instead of printing them.
Common mistakes
"Asynchronous means JavaScript runs in parallel."
Not on the main thread. "In parallel" means at the same time. The host can wait for several things at the same time. But your callbacks still run one at a time, on the main thread.
"A zero-delay timer runs right away."
It runs later. The current script, or the callback that is already running, finishes first.
"Every callback is asynchronous."
array.map and forEach call their callbacks right away, synchronously. What matters is the code that calls the callback.
"Async code makes slow work fast."
Waiting for the host can be async. But heavy work in JavaScript still blocks, unless you split it up or move it off the main thread.
"The host can stop my function in the middle."
Run to completion prevents that. A ready callback waits until the current code is done.
Practice: sync, async, and later callbacks
6 EXERCISESWithout running the code, write down the order of the three lines it prints. Then check your answer.
console.log("A: start");
setTimeout(() => {
console.log("B: timer callback");
}, 0);
console.log("C: end");The order is A: start, then C: end, then B: timer callback. Line 2 hands the timer to the host. JavaScript finishes the whole script before it runs the callback.
This function waits one second with a loop, so it blocks the page. Replace the loop with a host API, so the page keeps working while it waits. Which host API do you use?
function waitOneSecond() {
const started = Date.now();
while (Date.now() - started < 1000) {}
console.log("done");
}function waitOneSecond() {
setTimeout(() => {
console.log("done");
}, 1000);
}setTimeout asks the host to wait 1000 ms. JavaScript is free in the meantime, so the page keeps working. Then the callback prints done.
Do alert and prompt block the main thread? Answer yes or no, then explain.
Yes, they block. alert and prompt are browser tools that block the main thread. While the box is open, the page waits. That is why modern websites don't use them for normal messages.
Name two kinds of callbacks from this lesson that run later, not right away.
Some examples: a setTimeout callback, an event listener, and a handler for server data. The callback passed to array.map is not one. map calls it right away.
Can the timer callback interrupt the loop? Answer yes or no, then explain why.
setTimeout(() => {
console.log("timer callback");
}, 0);
console.log("loop starts");
for (let i = 0; i < 3; i = i + 1) {
console.log("loop " + i);
}
console.log("loop ends");No. The timer callback can't cut into the loop. JavaScript prints loop starts, the three loop lines, and loop ends. Only then does it print timer callback.
A search box should show "searching" now, then show the results when the server replies. What happens first, and what happens later? Why can you keep typing while it waits?
function showResults(results) {
console.log("showing " + results.length + " results");
}
setTimeout(() => showResults(["tea", "toast"]), 0);
console.log("searching...");function showResults(results) {
console.log("showing " + results.length + " results");
}
setTimeout(() => showResults(["tea", "toast"]), 0);
console.log("searching...");The page shows searching... now, and shows the two results later. The host does the waiting, so you can keep typing. showResults still runs on the main thread when its callback gets a turn.
Quiz: check your understanding
9 QUESTIONSRead each explanation, even when you got the answer right. A wrong choice often sounds almost right.
Question 1 of 9What does synchronous code mean?
Choose an answer to see the explanation.
Question 2 of 9In what order do the letters print?
Read the code, then predictconsole.log("A"); setTimeout(() => console.log("B"), 0); console.log("C");Choose an answer to see the explanation.
Question 3 of 9Who does the waiting for
setTimeout?Choose an answer to see the explanation.
Question 4 of 9Which callback usually runs right away, while the current code runs?
Choose an answer to see the explanation.
Question 5 of 9What does run to completion mean?
Choose an answer to see the explanation.
Question 6 of 9Which sentence about
asyncandawaitis true?Choose an answer to see the explanation.
Question 7 of 9Why can
JSON.parsefreeze a page when the text is huge?Choose an answer to see the explanation.
Question 8 of 9Which rule is safe to rely on?
Choose an answer to see the explanation.
Question 9 of 9What does this print?
Read the code, then predictasync function makeTea() { const drink = await Promise.resolve("tea"); console.log("ready: " + drink); } makeTea(); console.log("order placed");Choose an answer to see the explanation.
Key takeaways
- Synchronous code runs now. The next line waits until it is done.
- Blocking the main thread freezes the page. The browser draws the page and handles clicks on that same thread.
- Host APIs, such as timers, events,
fetch, and file reads, do the waiting outside JavaScript. - Callbacks given to host APIs run later. Callbacks given to
maprun now. - Run to completion: a ready callback waits until the current script or callback is done.
Remember the one-liner.
Sync code is like a phone call: you wait on the line. Async code is like a text: the host waits, and you handle the reply later.
Up next: Timers: setTimeout & setInterval.