Skip to main content

Command Palette

Search for a command to run...

Synchronous vs Asynchronous JavaScript

Updated
9 min read
Synchronous vs Asynchronous JavaScript
H
CS undergrad | Tech enthusiast | Focusing on Web Dev • DSA • ML | Building skills for real-world impact

JavaScript runs code line by line. But you constantly need to wait for things: files to load, API responses to arrive, timers to finish. Blocking code would freeze everything. Asynchronous code keeps things moving.

This is the core difference between JavaScript that feels fast and JavaScript that feels broken.


What Synchronous Code Means

Synchronous code runs line by line, in order. Each line waits for the previous one.

Simple Example

console.log("Line 1");
console.log("Line 2");
console.log("Line 3");

// Output:
// Line 1
// Line 2
// Line 3

One line, then the next. Straightforward.

With Operations

console.log("Start");
const sum = 5 + 3;
console.log("Sum:", sum);
console.log("Done");

// Output:
// Start
// Sum: 8
// Done

Each operation completes before moving on.

Real Example: Reading a File

const fs = require("fs");

console.log("Before");
const data = fs.readFileSync("data.txt", "utf8");
console.log("Data:", data);
console.log("After");

// Output:
// Before
// Data: (contents)
// After

readFileSync blocks. The program waits for the file.


What Asynchronous Code Means

Asynchronous code starts an operation but doesn't wait for it. It continues running and gets notified when the operation completes.

Simple Example

console.log("Start");

setTimeout(() => {
  console.log("After 2 seconds");
}, 2000);

console.log("End");

// Output:
// Start
// End
// (2 seconds pass)
// After 2 seconds

Notice: "End" prints before the timeout callback. JavaScript doesn't wait.

How It Works

Time →
|
Start: console.log("Start") → prints immediately
  |
  ├─ setTimeout called (doesn't wait)
  |  └─ callback scheduled for later
  |
  ├─ console.log("End") → prints immediately
  |
  └─ (user continues using app)
  |
  (2 seconds pass)
  |
  └─ Callback fires: console.log("After 2 seconds")
  |
Done

JavaScript continues running while waiting for the timer.

Real Example: API Request

console.log("Fetching data...");

fetch("https://api.example.com/users/1")
  .then(response => response.json())
  .then(user => console.log("User:", user));

console.log("Request sent");

// Output:
// Fetching data...
// Request sent
// (network request happens)
// User: { id: 1, name: "Alice" }

The fetch doesn't block. JavaScript continues to "Request sent" before the data arrives.


Synchronous Execution Timeline

START
  |
  v
Execute Line 1
  |
  v
Execute Line 2
  |
  v
Execute Line 3
  |
  v
Execute Line 4
  |
  v
END

Strict top-to-bottom order
Each line waits for the previous to finish

Asynchronous Task Queue Concept

START
  |
  v
Execute immediate code
  |
  ├─ Line 1: fetch("api")  → add to task queue
  |
  ├─ Line 2: console.log   → execute immediately
  |
  └─ Continue to END
  |
  v
QUEUE (waiting for data)
  |
  └─ fetch callback (waiting for response)
  |
  (when data arrives)
  |
  └─ Execute callback
  |
  v
END

Why JavaScript Needs Asynchronous Behavior

The Problem: Blocking Code

Without async, waiting for data freezes everything:

// Synchronous read from network (hypothetical)
const userData = synchronousNetworkRead("https://api.example.com/user/1");
// User can't do anything while waiting. App is frozen.

If the network takes 5 seconds, the entire app is frozen for 5 seconds. Users can't click, scroll, or interact.

The Solution: Asynchronous

// Asynchronous read from network
fetch("https://api.example.com/user/1")
  .then(response => response.json())
  .then(user => console.log("User:", user));

// User can still click, scroll, and interact
// App remains responsive

The fetch starts, but JavaScript keeps running. Users can interact while waiting.

Real-World Analogy

Synchronous (Restaurant): You order food. You stand at the counter and don't move until your food is ready. Everyone behind you waits. Nothing else happens.

Asynchronous (Pager): You order food. You get a pager. You sit down and can relax, order drinks, chat. When food is ready, the pager buzzes. You go get it. Everyone can use the line for their own orders.

JavaScript uses the pager model.


Examples: API Calls

Synchronous Approach (Hypothetical)

function getUserData(userId) {
  // This would freeze the app while waiting
  const user = synchronousAPICall(`/api/users/${userId}`);
  return user;
}

// User can't interact while waiting
const user = getUserData(1);
console.log("User:", user);

If the API takes 3 seconds, the app is frozen for 3 seconds.

Asynchronous Approach (Real)

function getUserData(userId) {
  return fetch(`/api/users/${userId}`)
    .then(response => response.json());
}

// App remains responsive
getUserData(1).then(user => {
  console.log("User:", user);
});

// This runs immediately, doesn't wait
console.log("Request started");

Output:

Request started
(network happens)
User: { id: 1, name: "Alice" }

The app stays responsive.

With Async/Await

async function getUserData(userId) {
  const response = await fetch(`/api/users/${userId}`);
  const user = await response.json();
  return user;
}

async function main() {
  const user = await getUserData(1);
  console.log("User:", user);
}

main();

Same thing, more readable. App stays responsive.


Examples: Timers

Synchronous Wait (Not Built-In)

JavaScript doesn't have a built-in synchronous wait, but you could simulate it:

function badWait(milliseconds) {
  const start = Date.now();
  while (Date.now() - start < milliseconds) {
    // Do nothing, just burn CPU
  }
}

console.log("Start");
badWait(3000);  // Block for 3 seconds
console.log("After 3 seconds");

This blocks everything. Users can't interact. CPU spins uselessly.

Asynchronous Wait (The Right Way)

console.log("Start");

setTimeout(() => {
  console.log("After 3 seconds");
}, 3000);

console.log("I run immediately");

// Output:
// Start
// I run immediately
// (3 seconds pass)
// After 3 seconds

The app stays responsive. Users can interact while waiting.

Real Scenario: Loading Multiple Files

Bad (Blocking):

const fs = require("fs");

console.log("Start");
const file1 = fs.readFileSync("file1.txt");  // Waits
const file2 = fs.readFileSync("file2.txt");  // Waits
const file3 = fs.readFileSync("file3.txt");  // Waits
console.log("Done");

// Synchronous reads = everything waits
// If each file takes 1 second, total = 3 seconds

Good (Non-Blocking):

const fs = require("fs").promises;

async function loadFiles() {
  console.log("Start");
  
  const [file1, file2, file3] = await Promise.all([
    fs.readFile("file1.txt"),
    fs.readFile("file2.txt"),
    fs.readFile("file3.txt")
  ]);
  
  console.log("Done");
}

loadFiles();

// Asynchronous reads = runs in parallel
// If each file takes 1 second, total = ~1 second
// App stays responsive the whole time

Same files, but async is faster and doesn't freeze the app.


Problems With Blocking Code

1. Frozen User Interface

Blocking code freezes the UI:

// Synchronous blocking code
function processLargeDataset() {
  let sum = 0;
  for (let i = 0; i < 1000000000; i++) {
    sum += i;
  }
  return sum;
}

// User clicks, types, hovers = nothing happens
const result = processLargeDataset();
console.log("Result:", result);

// Finally, UI responds after computation

During processLargeDataset(), the browser can't respond to clicks or updates.

2. Network Requests Block Everything

// Hypothetical blocking network code
function fetchUserBlocking(userId) {
  const user = blockingFetch(`/api/users/${userId}`);
  return user;
}

// This waits forever if network is slow
const user = fetchUserBlocking(1);

// Nothing else runs until the user data arrives

If the network is slow, everything is stuck.

3. Bad User Experience

// Blocking code
console.log("Saving data...");
const result = blockingSaveToDatabase(data);  // Waits
console.log("Data saved");

// User sees "Saving" for a long time
// Can't do anything else
// Feels slow and broken

4. Performance Issues

// Blocking: Load 3 files one at a time
// File 1: 1 second
// File 2: 1 second
// File 3: 1 second
// Total: 3 seconds

// Non-blocking: Load 3 files in parallel
// File 1, 2, 3: 1 second each (at the same time)
// Total: ~1 second

// Same files, 3x faster with async

Blocking code is slow because operations can't overlap.

5. No Responsiveness While Waiting

// Blocking: App is stuck
while (waitingForData) {
  // Nothing
}

// Non-blocking: App keeps running
fetch(url).then(data => {
  // Handle when ready
});

// User can scroll, click, interact while waiting

The JavaScript Event Loop

JavaScript handles async operations using an event loop:

Call Stack (immediate code)
  |
  ├─ Execute synchronous code here
  └─ When done, check Task Queue

Task Queue (async callbacks)
  |
  ├─ setTimeout callbacks waiting
  ├─ Promise callbacks waiting
  ├─ Fetch callbacks waiting
  └─ When stack is empty, run next task

Event Loop
  |
  └─ Continuously checks: if stack empty and queue has tasks?
       └─ Move task from queue to stack

Example:

console.log("1");

setTimeout(() => {
  console.log("2");
}, 0);

console.log("3");

// Output:
// 1
// 3
// 2

// Why?
// 1. "1" runs immediately (stack)
// 2. setTimeout scheduled (goes to queue)
// 3. "3" runs immediately (stack)
// 4. Stack empty, event loop checks queue
// 5. "2" runs from queue

Practice Assignment

1. Spot the blocking code:

// Is this blocking or non-blocking?
const data = readFileSync("data.txt");

// Answer: Blocking. App freezes while reading.

2. Convert blocking to non-blocking:

// Blocking version
const file1 = readFileSync("file1.txt");
const file2 = readFileSync("file2.txt");

// Convert to async

3. Understand the order:

console.log("A");
setTimeout(() => console.log("B"), 0);
console.log("C");

// What order does it print? Why?

4. Build a responsive app:

// Fetch data but keep UI responsive
// Show loading state while fetching
// Update UI when data arrives

Quick Recap

  • Synchronous code runs line by line, each line waits for the previous.

  • Asynchronous code starts operations but doesn't wait for them.

  • JavaScript uses the event loop to handle async operations.

  • Blocking code freezes the UI and makes apps feel slow.

  • Non-blocking code keeps the UI responsive.

  • Network requests, timers, and file operations should be async.

  • Synchronous file reads (readFileSync) block the entire app.

  • Asynchronous file reads keep everything responsive.

  • setTimeout, fetch, and promises are async.

  • async/await makes async code easier to read.

  • The task queue holds async callbacks until the call stack is empty.

Master the difference between synchronous and asynchronous, and you'll write JavaScript that feels fast and responsive.

Happy coding! 🚀


If you enjoyed this article, check out my other blogs on this profile. 🔗 Connect with me: LinkedIn | GitHub | X (Twitter)