Skip to content

Let JavaScript "Sleep" for a While

In many programming languages, such as Python, we can easily pause a program for 3 seconds using time.sleep(3). However, 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.

Let's look at our final implementation code, which is also the core of our discussion today:

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

/**
 * Main business logic: an asynchronous function
 */
async function run() {
    console.log('B: run function starts executing, preparing to serve the 1st customer.');
    let i = 0;
    while (i < 5) {
        i += 1;
        console.log(`C: [Loop ${i}] Ordering begins. Give 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 will leave here to execute other synchronous code
        await sleep(3000); 
        
        // After 3 seconds, the JS engine will return here to continue execution
        console.log(`E: [Loop ${i}] The food is ready! The waiter comes back and serves the food 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, and now he immediately returns to continue processing the main task, instead of waiting foolishly. The main task is completed.');

When running this code for the first time, the output order may 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 be executed completely before it's the turn of the subsequent code? To understand all this, we need to introduce the core execution model of JavaScript.

JS is Single-Threaded, Like a Restaurant

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

  • 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. The "order board" in his hand is the Call Stack, recording the tasks currently being executed.
  • Kitchen (Web APIs): An independent department with many chefs. They specialize in handling time-consuming tasks, such as network requests, file reading and writing, and our protagonist - setTimeout timer. The work of the kitchen does not take up the waiter's time.
  • Food Delivery Window (Task Queue / Callback Queue): The dishes made by the kitchen (callback functions after asynchronous tasks are completed) are placed here, waiting for the waiter to pick them up.
  • Waiter's Work Rules (Event Loop):
    1. Clear the Work at Hand: The waiter will prioritize processing all synchronous tasks on the "order board".
    2. Check the Food Delivery Window: When the "order board" is empty, the waiter will take a look at the "food delivery window".
    3. Add New Dishes: If there is food in the food delivery window, take one dish (a callback task) and put it on your "order board" to start serving.
    4. Loop Back and Forth: The waiter never rests and continues to repeat steps 2 and 3. This is the famous Event Loop.

The Cornerstone of async/await: Understanding Promise

The above sample code has async and await keywords, what are these, why use them?

Before we dive into the magic of async/await, we must first understand its cornerstone - Promise. async/await is just a "syntax sugar" that makes Promise more comfortable to use.

1. What is a Promise? - A Commitment to the Future

Imagine you go to a fast food restaurant to order food, the waiter will not let you wait in front of the counter for the burger to be made. He will give you a receipt, and say: "I promise that after the burger is made, come and pick it up with this receipt."

This receipt is the Promise. It represents an asynchronous operation that will only produce results in the future. It has three states:

  • Pending: You just got the receipt, and the burger is still being made. This is the initial state.
  • Fulfilled: The burger is ready! You can pick up the food with the receipt. This promise has been fulfilled.
  • Rejected: The kitchen found that there was no bread and couldn't make a burger. This promise was rejected.

2. How to use Promise? - .then() and .catch()

After getting the receipt, you won't just stand there, but can play with your phone first. But you need to know what to do next:

  • .then(onFulfilled): You tell yourself, "then, if the burger is ready (fulfilled), I will pick it up and eat it."
  • .catch(onRejected): You also made the worst plan, "If (catch) they tell me they can't make it (rejected), I will complain."

In code, it looks like this:

javascript
// This is an asynchronous operation that simulates "making a burger", it returns a Promise
function makeBurger() {
    return new Promise((resolve, reject) => {
        console.log('The kitchen starts making burgers...');
        // Simulate taking 2 seconds
        setTimeout(() => {
            if (Math.random() > 0.2) { // 80% success rate
                resolve('Delicious burger'); // Success! Call resolve
            } else {
                reject('No bread!'); // Failure! Call reject
            }
        }, 2000);
    });
}

// Order food and handle follow-up
makeBurger()
    .then(burger => {
        console.log('Successfully got:', burger);
    })
    .catch(error => {
        console.error('An error occurred:', error);
    });

console.log('I got the receipt, I\'ll go play with my phone first~');

Running this code, you will find that "I got the receipt..." will be printed immediately, and the result of the burger will be printed after 2 seconds. This is asynchronous!

3. The Relationship between Promise and sleep Function

Now look at our sleep function again, and it becomes clear:

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

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

4. Evolution from .then() to await

Using .then() chain calls to handle multiple asynchronous tasks can easily form "callback hell" when the logic is complex, and the readability of the code becomes poor.

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

async/await was born to solve this problem. await is like a magic button that can automatically "pause" and "wait" for the Promise receipt to be cashed, and then automatically execute the next line of code, making the asynchronous process look as clear as synchronous:

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('A total of 3 seconds passed');
}

Promise is the core object of JavaScript for handling asynchronous operations, it represents a promise. async/await is an elegant syntax for controlling the Promise process. Now, with a deep understanding of Promise, let's look at the previous "restaurant waiter model" again, and everything will be logical. await is waiting for the Promise receipt to change from "in progress" to "successful".

Code Execution Panoramic Perspective: Step-by-Step Tracking of the Waiter's Footprints

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

async function run() {
    console.log('B: run function starts executing, preparing to serve the 1st customer.');
    let i = 0;
    while (i < 5) {
        i += 1;
        console.log(`C: [Loop ${i}] Ordering begins. Give 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 will leave here to execute other synchronous code
        await sleep(3000); 
        
        // After 3 seconds, the JS engine will return here to continue execution
        console.log(`E: [Loop ${i}] The food is ready! The waiter comes back and serves the food 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, and now he immediately returns to continue processing the main task, instead of waiting foolishly. The main task is completed.');

Phase 1: Fast Processing of Main Tasks (T ≈ 0 seconds)

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

    • The waiter receives the first task: print log A. This is a synchronous task, and 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: run function starts executing, preparing to serve the 1st customer.
  4. while Loop (1st time) & console.log('C: ...'):

    • The waiter enters the loop and prints log C.
    • Console Output: C: [Loop 1] Ordering begins. Give the order to the chef...
  5. await sleep(3000) - Key Turning Point!:

    • The waiter encounters await. He did two things: a. Execute sleep(3000): This is equivalent to giving an order "remind me after 3 seconds" to the kitchen (Web APIs). The kitchen's timer starts timing, which is not the waiter's business. b. The await keyword tells the waiter: "The run function task is paused, you can leave!"
    • Result: The waiter "suspends" the run function, and then immediately withdraws and returns to the main task to see if there are any other things on his "order board".
  6. console.log('D: ...'):

    • The waiter finds that there is one last line of code in the main task that has not been executed! He handles it immediately.
    • Console Output: D: The waiter has given the 1st customer's order to the kitchen...
    • So far, we have explained why D is printed in advance. await does not block everything, it only pauses the async function it is in and returns control to the caller.

Phase 2: Waiting and Awakening (T = 0 to 3 seconds)

  • At this time, all the main synchronous code has been executed. The waiter's "order board" is empty.
  • According to the work rules, he starts constantly polling and checking the "food delivery window" (Task Queue).
  • The kitchen is timing the first sleep order, and the food delivery window is empty. The waiter is in an "idle but vigilant" waiting state.

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

  1. When T=3 seconds, setTimeout is completed:

    • The kitchen's timer rang! The chef put a notification "can continue to execute the run function" (that is, the resolve function) into the food delivery window (Task Queue).
  2. Event Loop Response:

    • The waiter immediately finds that there is a new task in the food delivery window! He retrieves this task and returns to where the run function was paused.
  3. run Function Resumes Execution:

    • console.log('E: ...'): The waiter continues to work from the next line of await and prints log E.
    • Console Output: E: [Loop 1] The food is ready! ...
  4. while Loop (2nd time):

    • The loop continues, and the waiter prints the next log C.
    • await sleep(3000) (2nd time): History repeats itself! The waiter once again hands over the new order to the kitchen, and is once 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 all the main tasks have already been completed.
    • He checks the food delivery window again. The kitchen just received a new order, and the food delivery window is also empty.
    • Therefore, 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 loop of "execution -> await -> pause -> wait -> wake up -> continue execution" will continue until the while loop ends.

  1. Loop ends, console.log('F: ...'):
    • When all 5 loops are completed, the run function continues to execute downward and prints the final log F.
    • At this time, the async function run is truly executed.

Deepen Knowledge Points

  1. async Keyword:

    • It marks an ordinary function as an asynchronous function.
    • Calling an async function will immediately return a Promise object without waiting for all the code inside the function to be executed.
  2. await Keyword:

    • Can only be used inside async functions.
    • It will pause the execution of the current async function and wait for the expression after it (usually a Promise) to become fulfilled.
    • await is the syntax sugar for asynchronous flow control, it makes asynchronous code look as intuitive as synchronous code, but its underlying layer is still based on Promise and event loop.
  3. The Essence of Non-Blocking: JavaScript's single thread implements a "non-blocking" concurrent model through the event loop mechanism. When encountering time-consuming tasks such as I/O operations or timers, the main thread will hand it over to other modules (such as Web APIs) for processing, and it will continue to execute subsequent code, thereby ensuring the smoothness of the UI and the responsiveness of the program. async/await is the superstructure of this model.

  4. Execution Timing Discrimination:

    • The code outside an async function will be executed immediately after the function's first await and yield control.
    • The code inside an async function after await must wait for the await Promise to complete, and then be put back into the main thread for execution through the scheduling of the event loop.