Skip to content

Let JavaScript "Sleep" for a While

In many programming languages, like Python, we can easily pause the program for 3 seconds using time.sleep(3). But in JavaScript, it's not that simple. If we use a "busy-waiting" loop to block the main thread, the entire browser page will freeze, which is absolutely unacceptable.

Our goal is to implement a non-blocking sleep function that can "pause" the execution of a piece of code without freezing the entire program.

Here is our final implementation, which is the core of today's discussion:

javascript
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

/**
 * Main business logic: an asynchronous function
 */
async function run() {
    console.log('B: The run function starts executing, ready to serve the 1st customer.');
    let i = 0;
    while (i < 5) {
        i += 1;
        console.log(`C: [Loop ${i}] Order taking starts. Hand the order to the chef and tell him to notify me when it's ready.`);
        
        // await will pause the run function, but the JS engine leaves here to execute other synchronous code
        await sleep(3000); 
        
        // After 3 seconds, the JS engine returns here to continue execution
        console.log(`E: [Loop ${i}] The dish is ready! The waiter returns and serves the dish to the customer.`);
    }

    console.log(`F: All 5 customers have been served, the run function has completely finished executing.`);
}

// --- Script Main Line (Global Scope) ---
console.log('A: The waiter (JS main thread) starts the day\'s work.');
run();
console.log('D: The waiter has given the 1st customer\'s order to the kitchen. Now he immediately returns to continue handling the main line tasks, instead of waiting foolishly. Main line tasks are complete.');

The first time you run this code, the output order might confuse beginners:

A -> B -> C (Loop 1) -> D -> (Wait 3 seconds) -> E (Loop 1) -> C (Loop 2) -> (Wait 3 seconds) -> E (Loop 2) -> C (Loop 3) -> (Wait 3 seconds) -> E (Loop 3) ... -> F

Why does D cut in line before E? Shouldn't the run() function finish executing before the subsequent code runs? To understand this, we need to introduce JavaScript's core execution model.

JS is Single-Threaded, Like a Server Restaurant

Imagine the world of JavaScript as an efficiently operating single-threaded restaurant:

  • The Waiter (JavaScript Main Thread / Call Stack): There is only one waiter in the restaurant. He is extremely fast but can only handle one thing at a time. His "order pad" is the Call Stack, recording the tasks currently being executed.
  • The Kitchen (Web APIs): An independent department with many chefs. They specialize in handling time-consuming tasks like network requests, file I/O, and our main character—the setTimeout timer. The kitchen's work does not occupy the waiter's time.
  • The Pickup Counter (Task Queue / Callback Queue): Dishes prepared by the kitchen (callback functions after asynchronous tasks complete) are placed here, waiting for the waiter to pick them up.
  • The Waiter's Work Rule (Event Loop):
    1. Finish Current Work: The waiter prioritizes completing all synchronous tasks on his "order pad."
    2. Check the Pickup Counter: When the "order pad" is empty, the waiter glances at the "pickup counter."
    3. Serve New Dishes: If there are dishes at the pickup counter, he takes one (a callback task) and places it on his "order pad" to start serving.
    4. Repeat: The waiter never rests, continuously repeating steps 2 and 3. This is the famous Event Loop.

The Foundation of async/await: Understanding Promise

The example code above contains the keywords async and await. What are they, and why use them?

Before we delve into the magic of async/await, we must first understand its foundation—Promise. async/await is just "syntactic sugar" that makes using Promise more comfortable.

1. What is a Promise? — A Promise for the Future

Imagine you order food at a fast-food restaurant. The waiter doesn't make you wait at the counter for the burger to be made. He gives you a receipt and says: "I promise (Promise), when the burger is ready, come get it with this receipt."

This receipt is the Promise. It represents an asynchronous operation whose result will be available in the future. It has three states:

  • Pending: You just got the receipt; the burger is still being made. This is the initial state.
  • Fulfilled: The burger is ready! You can pick it up with the receipt. The promise has been fulfilled.
  • Rejected: The kitchen ran out of buns and can't make the burger. The promise has been rejected.

2. How to Use a Promise? — .then() and .catch()

After getting the receipt, you don't just stand there; you can play on your phone. But you need to know what to do next:

  • .then(onFulfilled): You tell yourself, "Then, if the burger is ready (fulfilled), I'll go pick it up and eat it."
  • .catch(onRejected): You also prepare for the worst: "In case (catch) they tell me it can't be made (rejected), I'll go complain."

In code, it looks like this:

javascript
// This is an asynchronous operation simulating "making a burger," it returns a Promise
function makeBurger() {
    return new Promise((resolve, reject) => {
        console.log('Kitchen starts making the burger...');
        // Simulate taking 2 seconds
        setTimeout(() => {
            if (Math.random() > 0.2) { // 80% success rate
                resolve('A delicious burger'); // Success! Call resolve
            } else {
                reject('No buns left!');   // Failure! Call reject
            }
        }, 2000);
    });
}

// Place the order and handle the outcome
makeBurger()
    .then(burger => {
        console.log('Successfully got:', burger);
    })
    .catch(error => {
        console.error('Error:', error);
    });

console.log('I got the receipt, going to play on my phone~');

When you run this code, you'll see "I got the receipt..." printed immediately, and the burger result will print 2 seconds later. This is asynchrony!

3. The Relationship Between Promise and the sleep Function

Now, looking back at our sleep function, it becomes clear:

javascript
function sleep(ms) {
    // 1. new Promise: Returns a "promise receipt"
    return new Promise(resolve => {
        // 2. setTimeout: Hands an asynchronous task to the kitchen (Web APIs)
        //    The task is: call resolve() after ms milliseconds
        setTimeout(resolve, ms);
    });
}

sleep(3000) is promising: "I guarantee that after 3000 milliseconds, this promise will become fulfilled." It has no failure (reject) case; it's a simple promise that only succeeds.

4. The Evolution from .then() to await

Using .then() chaining to handle multiple asynchronous tasks can lead to "callback hell" when the logic is complex, making the code less readable.

javascript
// Callback hell style
sleep(1000)
    .then(() => {
        console.log('1 second passed');
        return sleep(1000); // Return a new Promise
    })
    .then(() => {
        console.log('Another second passed');
        return sleep(1000);
    })
    .then(() => {
        console.log('Total of 3 seconds passed');
    });

async/await emerged to solve this problem. await is like a magic button that automatically "pauses" and "waits" for that Promise receipt to be fulfilled, then automatically executes the next line of code, making asynchronous flow look as clear as synchronous code:

javascript
async function doSomething() {
    await sleep(1000);
    console.log('1 second passed');
    
    await sleep(1000);
    console.log('Another second passed');

    await sleep(1000);
    console.log('Total of 3 seconds passed');
}

Promise is the core object in JavaScript for handling asynchronous operations; it represents a promise. async/await is an elegant syntax for controlling Promise flow. Now, with a deep understanding of Promise, looking back at the "restaurant waiter model," everything falls into place. await is precisely waiting for that Promise receipt to change from "pending" to "fulfilled."

Code Execution Panorama: Step-by-Step Tracking of the Waiter's Footsteps

Now, let's follow the waiter step by step through the code execution flow.

async function run() {
    console.log('B: The run function starts executing, ready to serve the 1st customer.');
    let i = 0;
    while (i < 5) {
        i += 1;
        console.log(`C: [Loop ${i}] Order taking starts. Hand the order to the chef and tell him to notify me when it's ready.`);
        
        // await will pause the run function, but the JS engine leaves here to execute other synchronous code
        await sleep(3000); 
        
        // After 3 seconds, the JS engine returns here to continue execution
        console.log(`E: [Loop ${i}] The dish is ready! The waiter returns and serves the dish to the customer.`);
    }

    console.log(`F: All 5 customers have been served, the run function has completely finished executing.`);
}

// --- Script Main Line (Global Scope) ---
console.log('A: The waiter (JS main thread) starts the day\'s work.');
run();
console.log('D: The waiter has given the 1st customer\'s order to the kitchen. Now he immediately returns to continue handling the main line tasks, instead of waiting foolishly. Main line tasks are complete.');

Phase One: Rapid Processing of Main Line Tasks (T ≈ 0 seconds)

  1. console.log('A: ...'):

    • The waiter receives the first task: print log A. This is a synchronous task; he completes it immediately.
    • Console Output: A: The waiter (JS main thread) starts the day's work.
  2. run():

    • The waiter receives the second task: execute the run function. He immediately enters the run function.
  3. console.log('B: ...'):

    • Inside the run function, the waiter executes the first line of code, printing log B.
    • Console Output: B: The run function starts executing, ready to serve the 1st customer.
  4. while Loop (1st iteration) & console.log('C: ...'):

    • The waiter enters the loop and prints log C.
    • Console Output: C: [Loop 1] Order taking starts. Hand the order to the chef...
  5. await sleep(3000) - The Critical Turning Point!:

    • The waiter encounters await. He does two things: a. Executes sleep(3000): This is equivalent to handing a "remind me in 3 seconds" order to the Kitchen (Web APIs). The kitchen's timer starts counting down; this is no longer the waiter's concern. b. The await keyword tells the waiter: "Pause the run function task for now, you can leave!"
    • Result: The waiter "suspends" the run function and immediately leaves, returning to the main line tasks to check if there's anything else on his "order pad."
  6. console.log('D: ...'):

    • The waiter finds there's still one last line of code in the main line tasks not executed! He handles it immediately.
    • Console Output: D: The waiter has given the 1st customer's order to the kitchen...
    • This explains why D is printed early. await does not block everything; it only pauses the async function it's in and returns control to the caller.

Phase Two: Waiting and Waking (T = 0 to 3 seconds)

  • At this point, all main line synchronous code has been executed. The waiter's "order pad" is empty.
  • Following the work rule, he starts constantly polling the "pickup counter" (Task Queue).
  • The kitchen is timing the first sleep order; the pickup counter is empty. The waiter is in an "idle but alert" waiting state.

Phase Three: Callback of Asynchronous Tasks and Looping (T ≥ 3 seconds)

  1. At T=3 seconds, setTimeout completes:

    • The kitchen timer rings! The chef places a notification "can continue executing the run function" (i.e., the resolve function) into the Pickup Counter (Task Queue).
  2. Event Loop Response:

    • The waiter immediately notices a new task at the pickup counter! He retrieves this task and returns to where the run function was paused.
  3. run Function Resumes Execution:

    • console.log('E: ...'): The waiter continues working from the line after await, printing log E.
    • Console Output: E: [Loop 1] The dish is ready!...
  4. while Loop (2nd iteration):

    • The loop continues; the waiter prints the next log C.
    • await sleep(3000) (2nd time): History repeats! The waiter hands a new order to the kitchen and is again "kicked out" of the run function by await.
  5. What does the waiter do during the second pause?

    • He returns to the main line again. But the main line tasks were completed long ago.
    • He checks the pickup counter again. The kitchen just received a new order; the pickup counter is also empty.
    • So, from T=3.01 seconds to T=6 seconds, the waiter enters the "idle waiting" state again, waiting for the next setTimeout to complete.

This cycle of "execute -> await -> pause -> wait -> wake -> continue execution" repeats until the while loop ends.

  1. Loop Ends, console.log('F: ...'):
    • After all 5 loops complete, the run function continues downward, printing the final log F.
    • At this point, the async function run is truly finished executing.

Deepening Knowledge Points

  1. async Keyword:

    • It marks a regular function as an asynchronous function.
    • Calling an async function immediately returns a Promise object, without waiting for all the code inside the function to finish executing.
  2. await Keyword:

    • Can only be used inside an async function.
    • It pauses the execution of the current async function, waiting for the following expression (usually a Promise) to become fulfilled.
    • await is syntactic sugar for asynchronous flow control. It makes asynchronous code look as intuitive as synchronous code, but its underlying mechanism is still based on Promise and the event loop.
  3. The Essence of Non-Blocking: JavaScript's single-threaded nature, through the event loop mechanism, achieves a "non-blocking" concurrency model. When encountering I/O operations or timers and other time-consuming tasks, the main thread hands them off to other modules (like Web APIs) for processing, while itself continues executing subsequent code, thus ensuring UI fluidity and program responsiveness. async/await is the superstructure built upon this model.

  4. Execution Timing Analysis:

    • Code outside an async function will immediately get executed after the function first awaits and yields control.
    • Code inside an async function after an await must wait for the awaited Promise to complete and, through the scheduling of the event loop, be placed back onto the main thread for execution.