JavaScript Async Programming

Understanding asynchronous programming concepts and patterns

⚡ What is Async Programming?

Asynchronous programming allows JavaScript to perform long-running operations without blocking the main thread. This enables responsive user interfaces and efficient handling of I/O operations.


// Synchronous (blocking)
console.log("Start");
console.log("Middle");
console.log("End");

// Asynchronous (non-blocking)
console.log("Start");
setTimeout(() => console.log("Async operation"), 1000);
console.log("End");
                                    

Output:

Start

End

Async operation (after 1 second)

Async Programming Concepts

🔄

Event Loop

Manages execution of async operations

console.log("1");
setTimeout(() => console.log("2"), 0);
console.log("3");
📞

Callbacks

Functions passed to handle async results

function fetchData(callback) {
  setTimeout(() => callback("data"), 1000);
}
🤝

Promises

Objects representing future values

const promise = new Promise((resolve) => {
  setTimeout(() => resolve("done"), 1000);
});

Async/Await

Syntactic sugar for promises

async function getData() {
  const result = await fetchData();
  return result;
}

🔹 The Event Loop

Understanding how JavaScript handles asynchronous operations:

console.log("1: Synchronous");

setTimeout(() => {
    console.log("2: Timeout (macro task)");
}, 0);

Promise.resolve().then(() => {
    console.log("3: Promise (micro task)");
});

console.log("4: Synchronous");

// Execution order demonstrates event loop priority:
// 1. Synchronous code runs first
// 2. Micro tasks (Promises) run next  
// 3. Macro tasks (setTimeout) run last

Output:

1: Synchronous

4: Synchronous

3: Promise (micro task)

2: Timeout (macro task)

🔹 Async Patterns Comparison

Different ways to handle the same async operation:

// 1. Callback Pattern
function fetchUserCallback(id, callback) {
    setTimeout(() => {
        const user = { id: id, name: "John Doe" };
        callback(null, user);
    }, 1000);
}

// 2. Promise Pattern
function fetchUserPromise(id) {
    return new Promise((resolve) => {
        setTimeout(() => {
            const user = { id: id, name: "John Doe" };
            resolve(user);
        }, 1000);
    });
}

// 3. Async/Await Pattern
async function fetchUserAsync(id) {
    return new Promise((resolve) => {
        setTimeout(() => {
            const user = { id: id, name: "John Doe" };
            resolve(user);
        }, 1000);
    });
}

// Usage examples
fetchUserCallback(1, (err, user) => console.log("Callback:", user));
fetchUserPromise(2).then(user => console.log("Promise:", user));
fetchUserAsync(3).then(user => console.log("Async:", user));

Output (after 1 second each):

Callback: {id: 1, name: "John Doe"}

Promise: {id: 2, name: "John Doe"}

Async: {id: 3, name: "John Doe"}

🔹 Error Handling in Async Code

Proper error handling for different async patterns:

// Promise error handling
function riskyOperation() {
    return new Promise((resolve, reject) => {
        const success = Math.random() > 0.5;
        setTimeout(() => {
            if (success) {
                resolve("Operation successful!");
            } else {
                reject(new Error("Operation failed!"));
            }
        }, 1000);
    });
}

// Using .catch()
riskyOperation()
    .then(result => console.log("Success:", result))
    .catch(error => console.log("Error:", error.message));

// Using async/await with try-catch
async function handleRiskyOperation() {
    try {
        const result = await riskyOperation();
        console.log("Success:", result);
    } catch (error) {
        console.log("Error:", error.message);
    }
}

handleRiskyOperation();

Output (randomly):

Success: Operation successful!

OR

Error: Operation failed!

🔹 Parallel vs Sequential Execution

Control the timing of multiple async operations:

// Sequential execution (one after another)
async function sequential() {
    console.time("Sequential");
    
    const result1 = await new Promise(resolve => 
        setTimeout(() => resolve("Task 1"), 1000)
    );
    const result2 = await new Promise(resolve => 
        setTimeout(() => resolve("Task 2"), 1000)
    );
    const result3 = await new Promise(resolve => 
        setTimeout(() => resolve("Task 3"), 1000)
    );
    
    console.timeEnd("Sequential"); // ~3000ms
    return [result1, result2, result3];
}

// Parallel execution (all at once)
async function parallel() {
    console.time("Parallel");
    
    const [result1, result2, result3] = await Promise.all([
        new Promise(resolve => setTimeout(() => resolve("Task 1"), 1000)),
        new Promise(resolve => setTimeout(() => resolve("Task 2"), 1000)),
        new Promise(resolve => setTimeout(() => resolve("Task 3"), 1000))
    ]);
    
    console.timeEnd("Parallel"); // ~1000ms
    return [result1, result2, result3];
}

// Test both approaches
sequential().then(results => console.log("Sequential results:", results));
parallel().then(results => console.log("Parallel results:", results));

Output:

Sequential: 3000ms

Parallel: 1000ms

Sequential results: ["Task 1", "Task 2", "Task 3"]

Parallel results: ["Task 1", "Task 2", "Task 3"]

🧠 Test Your Knowledge

What runs first in the event loop?