β³ Synchronous vs Asynchronous Code
JavaScript does one thing at a time β yet the apps you use every day fetch data, animate, and respond to clicks all at once. The trick is knowing when code should wait and when it should keep going. This lesson makes that distinction concrete so your interfaces never freeze.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain what it means for code to be synchronous (blocking) versus asynchronous (non-blocking)
- Describe why JavaScript's single thread makes blocking code so dangerous in the browser
- Trace the execution order of a program that mixes synchronous statements with
setTimeout - Decide when a synchronous approach is fine and when an asynchronous one is required
- Build a small demo that visibly freezes the page, then fix it with a non-blocking version
Estimated Time: 25β35 minutes β’ Difficulty: BeginnerβIntermediate
Hands-on: Build two buttons β one that locks up the browser, one that stays responsive.
In This Lesson
Two Ways to Run Code
Every line of code you write eventually has to run. The question this lesson answers is a simple one with huge consequences: does the program stop and wait for a task to finish, or does it start the task and move on? Those two answers are called synchronous and asynchronous execution.
β A coffee-shop analogy. In a synchronous cafΓ©, each customer orders and then stands at the counter until their drink is finished before the next person can even order. In an asynchronous cafΓ©, you order, get a buzzer, and step aside β the barista starts several drinks at once and buzzes you when yours is ready. Same barista, dramatically different throughput.
Neither model is "better" in the abstract β each fits different jobs. The skill you're building is recognizing which one a situation calls for.
Synchronous: One Thing at a Time
In synchronous code, statements run in order, top to bottom, and each one must finish completely before the next begins. While a synchronous operation is running, nothing else can happen β we say it blocks.
Key characteristics
- Blocking: a long operation holds up everything behind it.
- Predictable order: the code runs in exactly the sequence you read it.
- Easy to reason about: there is no "later" β line 3 always happens after line 2.
A blocking example
console.log('Starting');
function sumTo(n) {
let sum = 0;
for (let i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
// A huge loop that keeps the CPU busy for a noticeable moment
const result = sumTo(2_000_000_000); // blocks here
console.log('Result:', result);
console.log('Finished'); // will NOT print until the loop above is done
Run this in a browser and the tab freezes while sumTo churns: you can't scroll, click, or type. The string 'Finished' only appears once the loop completes, because the single thread is fully occupied.
π Key Terms
Blocking: an operation that prevents any other code from running until it completes.
Thread: a single sequence of instructions being executed. JavaScript runs your code on one main thread.
Main thread: the thread that also handles rendering and user input β the one you must never block for long.
Synchronous, everyday analogies: reading a book page by page, standing in a single-file line, or a one-lane road where a slow truck delays everyone behind it.
Asynchronous: Start Now, Finish Later
Asynchronous code lets you kick off a task and immediately continue with the rest of your program. When the task eventually finishes, its follow-up code runs. The program never stands around waiting.
Key characteristics
- Non-blocking: slow work happens "in the background," so the main thread stays free.
- Deferred: the follow-up code runs after the current work finishes, not in source order.
- Responsive: the UI keeps reacting to the user the whole time.
The classic surprise
console.log('Starting');
setTimeout(() => {
console.log('This runs after ~2 seconds');
}, 2000);
console.log('This runs immediately');
// Console output:
// Starting
// This runs immediately
// This runs after ~2 seconds (about 2s later)
Notice the output order does not match the source order. setTimeout hands its callback to the browser to run later, then execution moves straight on to the next line. The 2000 is a minimum delay, not a guarantee β the callback only runs once the main thread is free (you will see exactly why in the next lesson on the event loop).
β οΈ A common misconception
Asynchronous does not mean "multi-threaded" or "runs in parallel." Your JavaScript still runs on one thread. Async simply means the slow part (the timer, the network request) is handed off, and your callback is queued to run later on that same single thread.
Asynchronous, everyday analogies: sending an email and carrying on with your day, or starting a washing machine and cooking dinner while it runs.
Why This Matters in JavaScript
Here is the crux: JavaScript in the browser runs your code on a single main thread β and that same thread is responsible for painting the screen and reacting to clicks, scrolls, and keypresses. If you block it with a long synchronous task, the page cannot repaint or respond. It freezes.
Async work you meet constantly
- Network requests β fetching data from an API or server
- Timers β
setTimeoutandsetInterval - User events β clicks, keystrokes, scrolls
- File and database access β reading, writing, and querying (especially in Node.js)
- Animations β smooth visual changes over time
π‘ The golden rule
Keep the main thread free. Any task that could take more than a few milliseconds β a network call, a big computation β should be asynchronous (or chunked), so the interface never locks up.
Worked Example: Loading a Profile
Imagine a page that needs a user's data and their profile image from a server. Let's contrast a naive blocking design with a responsive asynchronous one.
The blocking version (don't do this)
console.log('App started');
// Pretend these are synchronous, blocking network calls.
// In real code they do not exist β this is the anti-pattern.
const userData = getUserDataBlocking(); // imagine 2 seconds, UI frozen
console.log('User data loaded');
const userImage = getUserImageBlocking(); // imagine 3 more seconds, still frozen
console.log('User image loaded');
renderProfile(userData, userImage);
// Total: ~5 seconds of a completely unresponsive page
For five whole seconds the user cannot do anything. Most people assume the app has crashed.
The asynchronous version (do this)
console.log('App started');
// Modern async: fetch returns a Promise; await pauses THIS function
// without blocking the main thread.
async function loadProfile() {
const [userData, userImage] = await Promise.all([
fetch('/api/user').then((res) => res.json()),
fetch('/api/user/avatar').then((res) => res.blob()),
]);
renderProfile(userData, userImage);
console.log('Profile rendered');
}
loadProfile();
console.log('UI is interactive while data loadsβ¦');
What the user experiences
App started
UI is interactive while data loadsβ¦
Profile rendered (once both requests resolve)
Two important wins: the page stays responsive the entire time, and by requesting both resources with Promise.all they load concurrently instead of one-after-another. You'll study fetch, Promises, and async/await in depth soon β for now, notice the shape: start the slow work, don't block, react when it's done.
When to Use Each
Async is powerful, but it adds complexity β execution order becomes less obvious. Reach for it deliberately, not reflexively.
| Prefer synchronous when⦠| Prefer asynchronous when⦠|
|---|---|
| The work is fast and CPU-only (formatting a string, math on a small array) | You perform I/O β network, disk, or database access |
| Each step truly depends on the previous step's result and there's nothing else to do | An operation may take a noticeable amount of time |
| Simplicity and readability outweigh everything else | Several independent operations can run concurrently |
| You're working with small in-memory data | You must keep a user interface responsive |
β Rule of thumb
If the task leaves the process (talks to a server, disk, or another service) or could take longer than a frame (~16 ms), make it asynchronous. Pure, quick, self-contained logic can stay synchronous.
Hands-on Exercise
ποΈ Feel the Freeze: Two Buttons
Objective: Experience blocking versus non-blocking code first-hand by building a page where one button freezes the browser and another does not.
Instructions:
- Create an HTML file with two buttons and an output area:
<button id="syncBtn">Run 5 Blocking Tasks</button> <button id="asyncBtn">Run 5 Non-blocking Tasks</button> <input placeholder="try typing while each runs" /> <pre id="out"></pre> - Add the script below. The blocking task busy-waits (burns CPU) for one second; the non-blocking one uses
setTimeout.const out = document.getElementById('out'); const log = (msg) => { out.textContent += msg + '\n'; }; // Blocking: occupies the thread for ~1 second function blockingTask(n) { const end = Date.now() + 1000; while (Date.now() < end) { /* burn CPU */ } return `Task ${n} done`; } // Non-blocking: schedules completion for ~1 second later function asyncTask(n, done) { setTimeout(() => done(`Task ${n} done`), 1000); } document.getElementById('syncBtn').addEventListener('click', () => { out.textContent = 'Blocking start (try typing now!)\n'; for (let i = 1; i <= 5; i++) { log(blockingTask(i)); } log('All blocking tasks done'); }); document.getElementById('asyncBtn').addEventListener('click', () => { out.textContent = 'Non-blocking start (try typing now!)\n'; let finished = 0; for (let i = 1; i <= 5; i++) { asyncTask(i, (msg) => { log(msg); if (++finished === 5) log('All non-blocking tasks done'); }); } }); - Click each button and β this is the point β try typing in the text field while it runs.
π‘ Hint
With the blocking button, the input ignores your keystrokes for ~5 seconds and all results appear at once. With the non-blocking button, you can keep typing and the results trickle in. If you don't feel the freeze, increase the busy-wait to 1500 ms.
β What you should observe
The blocking version prints all five lines together after the page unfreezes β the thread was too busy to repaint. The async version keeps the input live and logs each task as its timer fires. Same five tasks, completely different user experience. Real network code behaves like the async button.
π― Quick Quiz
Question 1: What does it mean for a synchronous operation to "block"?
Question 2: What is the console output order of this snippet?
console.log('A'); setTimeout(() => console.log('B'), 0); console.log('C');
Question 3: Which task is the best candidate for an asynchronous approach?
Summary & Quiz
π Key Takeaways
- Synchronous code runs in order and blocks β each step must finish before the next starts.
- Asynchronous code starts a task and moves on, running follow-up code later β so execution order is not always source order.
- Browser JavaScript uses a single main thread shared with rendering and input, so long blocking tasks freeze the page.
- Async does not mean multi-threaded; the slow part is offloaded and its callback is queued on the same thread.
- Use async for I/O and anything slow; synchronous is fine for quick, in-memory logic.
π Further Reading
π What's Next?
You now know that async code runs later β but how does JavaScript decide the exact moment a deferred callback runs? That's the job of the event loop, and it's next.
π Great start!
You can now explain why a page freezes β and how to stop it. Let's look under the hood.