← Back to postsCoding Notes
EnglishPublished Feb 27, 2026Updated Feb 27, 20268 min read

The Night Shift Cook & the Single-Threaded Kitchen

Tips

A candid guide to synchronous vs asynchronous JavaScript for engineers who want the real mental model, not just syntax.

Act 1 — The Setup: Imagine a Restaurant with One Cook

It's Friday night. The restaurant is packed. There's one cook in the kitchen — talented, fast, meticulous — but still just one person. This is JavaScript. Single-threaded. One call stack. One cook who can only do one thing at a time.

The manager has two choices for how this kitchen runs. And this choice defines everything about whether the restaurant survives the Friday night rush.

The core question of async JS isn't about speed or parallelism. It's about how a single cook can keep the kitchen moving without freezing at every order.


Act 2 — The Blocking Cook: Synchronous Mode

In the synchronous world, the cook takes order #1, starts cooking it, stands over the stove for 3 minutes watching it, plates it, then — and only then — takes order #2. Everyone else waits. The line goes out the door.

javascript
// sync-kitchen.js — The Blocking Cook

const processOrder = (customer) => {
  console.log(`Processing order for ${customer}`);
  console.log(`Order in progress for ${customer}`);

  // 🚨 Blocking: The cook just STARES at the stove for 3 seconds.
  // JS engine is completely FROZEN. Nothing else can happen.
  const startTime = Date.now();
  while (Date.now() - startTime <= 3000) {
    // Spinning wheels... burning CPU... blocking the ENTIRE thread
  }

  console.log(`✅ Cooking completed for ${customer}`);
};

console.log("Take order for Customer 1");
processOrder("Customer 1");                  // 🧱 ENTIRE JS THREAD BLOCKS HERE
console.log("Take order for Customer 2");   // This runs only after 3 seconds

Console Output:

code
Take order for Customer 1
Processing order for Customer 1
Order in progress for Customer 1
[ ... 3 second freeze — thread is dead ... ]
✅ Cooking completed for Customer 1
Take order for Customer 2    ← only NOW does this run

Timeline — Synchronous (Blocking):

code
0ms         → Take Order 1
0ms–3000ms  → 🧱 processOrder("Customer 1") — THREAD FROZEN
3000ms+     → Take Order 2 (finally)

⚠️ This is not a performance trick gone wrong. This is the literal call stack getting stuck. While the while-loop runs, your browser can't repaint, can't handle clicks, can't do anything. Users think the tab has crashed. Because for all practical purposes — it has.


Myth Busters — Kill These Right Now

Before going further, let's address the questions you probably Googled before reading this.

❌ Myth✅ Fact
"Async makes JavaScript run in parallel / multi-threaded"JS is still single-threaded. Async doesn't add threads. It delegates waiting to the browser/Node environment.
"Async code runs faster because it's concurrent"Async doesn't speed up code. It keeps your thread free and responsive while waiting. The work takes the same time — you just don't block on it.
"setTimeout runs code after exactly N milliseconds"setTimeout adds a callback to the queue after N ms, but the callback only runs when the call stack is empty. It could be delayed longer.
"async function means the code inside runs asynchronously"An async function runs synchronously until it hits an await. The async keyword just means it returns a Promise.

Act 3 — The Real Secret: The Event Loop (The Kitchen Pager System)

Here's the real mental model. JavaScript doesn't get async superpowers from the language spec. It gets them from the environment it runs in — the browser or Node.js — which has access to operating system threads, timers, and I/O APIs.

Think of it this way: the cook (JS engine) takes an order that needs 3 minutes in the oven. Instead of standing there watching it, the cook puts it in the oven, sets a timer, and goes back to the counter to take the next order. When the timer goes off, the kitchen pager rings, and the cook finishes the plating when they're free.

code

┌─────────────────┐    ┌──────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   Call Stack    │ →  │  Web APIs        │ →  │  Callback Queue  │ →  │   Event Loop    │
│                 │    │  (Browser/Node)  │    │                  │    │                 │
│  processOrder() │    │  setTimeout ⏱️   │    │  () => done      │    │  Monitors stack │
│  main script    │    │  fetch 🌐        │    │                  │    │  If empty→push  │
└─────────────────┘    │  DOM Events 🖱️   │    └──────────────────┘    └─────────────────┘
                       └──────────────────┘

The Event Loop does one job with religious devotion: it checks if the call stack is empty. If it is, it picks the next callback from the queue and pushes it onto the stack. That's it. That's the whole trick.


Act 4 — The Non-Blocking Cook: setTimeout Unlocks It

Now let's rewrite our kitchen. The cook takes the order, puts it in the oven (delegates to the browser's timer API), and immediately moves on. When the oven beeps (callback arrives in the queue), the cook finishes the order — but only when the current task is done.

javascript
// async-kitchen.js — The Non-Blocking Cook

const processOrder = (customer) => {
  console.log(`Processing order for ${customer}`);
  console.log(`Order in progress for ${customer}`);

  // 🍳 "Put it in the oven" — delegate to browser timer API
  // The cook DOES NOT WAIT here. Thread is FREE immediately.
  setTimeout(() => {
    console.log(`✅ Cooking completed for ${customer}`);
  }, 3000);

  // Execution continues immediately ↓
};

console.log("Take order for Customer 1");
processOrder("Customer 1");           // Registers timer, returns immediately
console.log("Take order for Customer 2");  // Runs WITHOUT waiting for Customer 1's food

Console Output:

code
Take order for Customer 1
Processing order for Customer 1
Order in progress for Customer 1
Take order for Customer 2            ← runs immediately, no waiting!
[ ...3 seconds pass in the background... ]
✅ Cooking completed for Customer 1  ← callback fires when stack is empty

Timeline — Asynchronous (Non-Blocking):

code
0ms          → Take Order 1 + Register Timer
~5ms         → Take Order 2 — Thread FREE ✓
0ms–3000ms   → Thread is FREE — browser handles timer in background
3000ms+      → ✅ Callback: Cooking Completed

"Async doesn't make JavaScript run faster. It makes JavaScript stop blocking itself."


Act 5 — Controlling Flow with Callbacks (And Their Dark Side)

Once you start going async, you need a way to say "after step A completes, do step B." The first tool JavaScript gave us was callbacks — just pass a function to be called when the work is done.

javascript
// callbacks.js — Restaurant Flow with Callbacks

const takeOrder = (customer, callback) => {
  console.log(`Take order for ${customer}`);
  callback(customer);  // "Here's your order ticket, kitchen!"
};

const processOrder = (customer, callback) => {
  console.log(`Processing order for ${customer}`);
  setTimeout(() => {
    console.log(`Cooking completed for ${customer}`);
    callback(customer);  // "Food's ready, waiter — complete the order!"
  }, 3000);
};

const completeOrder = (customer) => {
  console.log(`✅ Complete order for ${customer}`);
};

// Chaining callbacks together to control flow:
takeOrder("Customer 1", (customer) => {
  processOrder(customer, (customer) => {
    completeOrder(customer);  // Nested 2 levels deep — manageable
  });
});

// Now imagine adding: payment → loyalty points → email receipt → SMS → ...
// This becomes "Callback Hell" or the "Pyramid of Doom" 🔺

⚠️ The Callback Hell Problem: Callbacks work, but they don't scale. Add 5 steps and you have 5 levels of nesting. Error handling becomes a nightmare (every callback needs its own error check). Reading the code becomes archaeological digging. This is why Promises were born.


Act 6 — Promises: The Kitchen Buzzer System

A Promise is an object that represents a future value. Think of it as the buzzer a restaurant gives you when you're waiting for a table — it hasn't gone off yet, but it will either go off (resolve) or the restaurant closes (reject).

A Promise is always in one of three states:

  • Pending — still cooking
  • Fulfilled (resolved) — food is ready ✅
  • Rejected — kitchen fire, order cancelled ❌
javascript
// promises.js — Meeting Scheduler

const hasMeeting = false;

// A function that RETURNS a promise (preferred pattern)
const meeting = () => {
  return new Promise((resolve, reject) => {
    if (hasMeeting === false) {
      const meetingDetails = {
        name: "Project Meeting",
        place: "Google Meet",
        time: "10:00 AM",
      };
      resolve(meetingDetails);  // ✅ Buzzer goes off — here's your table
    } else {
      reject(new Error("Meeting already scheduled"));  // 🔴 Restaurant is full
    }
  });
};

meeting()
  .then((res) => {
    console.table(res);    // Runs on resolve ✅
  })
  .catch((err) => {
    console.log(err.message); // Runs on reject ❌
  });

// ⚠️ Difference: new Promise(...) executes IMMEDIATELY when created.
// () => new Promise(...) executes only when you call it.
// Always prefer the function-returning pattern for reusability.

Promises Fix the Nesting — Promise Chains

Instead of nested callbacks, each step returns a Promise and we chain with .then(). Linear. Readable. Each .then() receives what the previous Promise resolved with.

javascript
// promise-chain.js — Restaurant Flow with Promises

const takeOrder = (customer) => {
  return new Promise((resolve) => {
    console.log(`Take order for ${customer}`);
    resolve(customer);  // Pass customer downstream
  });
};

const processOrder = (customer) => {
  return new Promise((resolve) => {
    console.log(`Processing order for ${customer}`);
    setTimeout(() => {
      console.log(`Cooking completed for ${customer}`);
      resolve(customer);  // ✅ Resolve AFTER the async work is done
    }, 3000);
    // ⚠️ Note: resolve() must be INSIDE setTimeout, not after it.
    // Calling resolve() outside would resolve BEFORE cooking finishes.
  });
};

const completeOrder = (customer) => {
  console.log(`✅ Complete order for ${customer}`);
};

// Clean, readable, FLAT chain — no nesting:
takeOrder("Customer 1")
  .then(processOrder)       // processOrder receives "Customer 1"
  .then(completeOrder)      // completeOrder receives "Customer 1"
  .catch((err) => {
    console.log(err.message); // ONE catch handles errors from ANYWHERE in the chain
  });

Act 7 — async / await: Synchronous-Looking, Asynchronous Behavior

Promises are great. But chains with .then() can still get messy when you need to share variables between steps or have conditional logic. async/await is syntactic sugar over Promises — it makes async code look and read like synchronous code, without actually blocking the thread.

javascript
// async-await.js — The Gold Standard Pattern

const runRestaurant = async () => {
  try {
    // await pauses THIS function (not the thread) until the Promise settles
    const customer = await takeOrder("Customer 1");
    const cooked   = await processOrder(customer);
    completeOrder(cooked);
  } catch (err) {
    console.log("Something went wrong:", err.message);
  }
};

runRestaurant();
console.log("This runs IMMEDIATELY — runRestaurant is non-blocking");

What await actually does: It pauses the execution of the enclosing async function and returns control to the caller. The event loop continues running. Other callbacks can fire. When the awaited Promise settles, the async function resumes from that point. Your thread never actually blocks.

The Dangerous Misconception About async/await

This is where a lot of engineers trip up — including senior ones.

javascript
// misconception.js — This Does NOT Work as Expected

async function test() {
  setTimeout(() => {
    console.log("hello");
  }, 3000);
  // ⚠️ Returns undefined — NOT a Promise wrapping the setTimeout
}

const tt = await test();
// tt is undefined. The await resolved immediately because test()
// returned a Promise that resolved with undefined right away.
// The setTimeout callback still fires 3 seconds later, independent.
// "hello" WILL print — but await didn't wait for it.

// ✅ CORRECT way to await a timer:
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function testCorrect() {
  await delay(3000);         // ✅ NOW we're awaiting a real Promise
  console.log("hello");     // This runs after 3 seconds
}

⚠️ Key insight: You can only await something useful if it returns a Promise. setTimeout doesn't return a Promise. If you wrap setTimeout in an async function, the async function's implicit Promise resolves immediately — it doesn't wait for the timer. Always wrap timer-based async in a Promise explicitly.


The Full Picture — Side by Side

ApproachPatternError HandlingReadabilityUse When
SyncInline code, while looptry/catch⭐⭐⭐⭐⭐CPU-bound, no I/O, tiny scripts
CallbacksPass function as argumentError-first pattern⭐⭐ (nests fast)Legacy code, simple event listeners
Promises.then() / .catch() chainOne .catch() at end⭐⭐⭐⭐Chained sequential I/O, Promise.all
async/awaitawait expressiontry/catch⭐⭐⭐⭐⭐Everything — the modern default

Bonus Round — Promise.all: Parallel Ordering (Sort Of)

If you need multiple async operations that don't depend on each other, you can run them "in parallel" — not by using multiple threads, but by firing them all off before awaiting any of them.

javascript
// parallel.js — Taking 3 Orders Simultaneously

// ❌ SEQUENTIAL — takes 9 seconds total
const r1 = await processOrder("C1"); // wait 3s
const r2 = await processOrder("C2"); // wait another 3s
const r3 = await processOrder("C3"); // wait another 3s

// ✅ CONCURRENT — fires all 3, waits for slowest — ~3 seconds total
const [r1, r2, r3] = await Promise.all([
  processOrder("C1"),  // All 3 start immediately
  processOrder("C2"),
  processOrder("C3"),
]);

// 🚨 If ANY one rejects, Promise.all rejects immediately.
// Use Promise.allSettled() if you want all results regardless of failure.

The three processOrder calls each delegate to the browser's timer API. All three timers run "simultaneously" in the browser's environment (not in the JS thread). The JS thread just waits for all three Promises to resolve. This is the closest thing to "parallel" you'll get in JavaScript without Web Workers.


The TL;DR — Mental Models That Will Stick

JavaScript has one cook. That cook can either stare at the stove (synchronous blocking), or put things in the oven and come back to them (asynchronous non-blocking). Async doesn't hire more cooks — it just makes the one cook more efficient by not making them wait.

The Event Loop is the kitchen pager. Callbacks sit in a queue. The pager (event loop) only hands work to the cook when the cook has nothing in hand (call stack is empty). This is why setTimeout(fn, 0) doesn't run immediately — it runs after the current stack clears.

Callbacks → Promises → async/await is an evolution of ergonomics, not capabilities. They all solve the same problem. async/await is just the clearest way to express it. Under the hood, async/await compiles to Promises. Promises use callbacks internally.

async doesn't mean parallel. It means non-blocking. For true parallelism in a browser context, look at Web Workers. In Node.js, look at worker_threads or child_process. These actually spawn OS threads. Regular async JS does not.

The moment this clicks is when you stop asking "why did my callback run before/after X?" and start asking "what was on the call stack at that moment, and what was waiting in the queue?" That's the event loop. That's the whole game.