4.7: Asynchronous JavaScript

Master form validation and user input handling using JavaScript. Learn how to validate email addresses, check required fields, provide user feedback, and create robust, user-friendly forms.

1. Synchronous vs Asynchronous

Synchronous (Blocking)

Code executes line by line, waiting for each operation to complete:

console.log("First");
console.log("Second");
console.log("Third");

// Output:
// First
// Second
// Third

Problem: Slow operations block everything:

console.log("Start");

// Simulate slow operation (blocking)
for (let i = 0; i < 1000000000; i++) {}  // Takes 1-2 seconds

console.log("End");
// Page freezes during loop!

Python comparison:

# Python is also synchronous by default
print("First")
print("Second")
print("Third")

# Blocking operation
import time
time.sleep(2)  # Blocks for 2 seconds
print("After sleep")

Asynchronous (Non-blocking)

Operations run in the background, allowing other code to execute:

console.log("Start");

setTimeout(() => {
  console.log("Delayed message");
}, 2000);  // Wait 2 seconds

console.log("End");

// Output:
// Start
// End
// Delayed message (after 2 seconds)

Common async operations:

  • Network requests (fetch API)
  • File reading (Node.js)
  • Timers (setTimeout, setInterval)
  • Database queries
  • User interactions (events)

2. Callbacks

Callback = Function passed as argument to another function, executed later.

Basic Callback

function greet(name, callback) {
  console.log(`Hello, ${name}`);
  callback();
}

greet("Alice", () => {
  console.log("Callback executed!");
});

// Output:
// Hello, Alice
// Callback executed!

Python comparison:

def greet(name, callback):
    print(f"Hello, {name}")
    callback()

greet("Alice", lambda: print("Callback executed!"))

Async Callbacks

console.log("Start");

setTimeout(() => {
  console.log("Timeout callback");
}, 1000);

console.log("End");

// Output:
// Start
// End
// Timeout callback (after 1 second)

Callback Hell (Pyramid of Doom)

Problem: Nested callbacks become unreadable:

setTimeout(() => {
  console.log("Step 1");
  setTimeout(() => {
    console.log("Step 2");
    setTimeout(() => {
      console.log("Step 3");
      setTimeout(() => {
        console.log("Step 4");
        // Keep nesting... 😱
      }, 1000);
    }, 1000);
  }, 1000);
}, 1000);

Python asyncio comparison:

import asyncio

async def steps():
    await asyncio.sleep(1)
    print("Step 1")
    await asyncio.sleep(1)
    print("Step 2")
    # Much cleaner!

Solution: Use Promises or async/await (covered next).


3. Promises

Promise = Object representing eventual completion (or failure) of an async operation.

Promise States

A promise has three states:

  1. Pending - Initial state, not fulfilled or rejected
  2. Fulfilled - Operation completed successfully
  3. Rejected - Operation failed

Creating a Promise

const promise = new Promise((resolve, reject) => {
  // Async operation
  const success = true;

  if (success) {
    resolve("Success!");  // Fulfill promise
  } else {
    reject("Error!");     // Reject promise
  }
});

promise
  .then(result => {
    console.log(result);  // "Success!"
  })
  .catch(error => {
    console.log(error);
  });

Real Example: Simulated API Call

function fetchUser(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (userId > 0) {
        resolve({ id: userId, name: "Alice" });
      } else {
        reject("Invalid user ID");
      }
    }, 1000);
  });
}

fetchUser(1)
  .then(user => {
    console.log("User:", user);
  })
  .catch(error => {
    console.log("Error:", error);
  });

Chaining Promises

fetchUser(1)
  .then(user => {
    console.log("User:", user.name);
    return user.id;  // Pass to next then()
  })
  .then(userId => {
    console.log("User ID:", userId);
    return userId * 2;
  })
  .then(result => {
    console.log("Result:", result);
  })
  .catch(error => {
    console.log("Error:", error);
  });

// Output:
// User: Alice
// User ID: 1
// Result: 2

Key point: Each .then() returns a new promise, allowing chaining.

finally()

Runs regardless of success or failure:

fetchUser(1)
  .then(user => console.log("User:", user))
  .catch(error => console.log("Error:", error))
  .finally(() => {
    console.log("Cleanup done");  // Always runs
  });

4. Async/Await (Modern Approach)

async/await = Syntactic sugar over promises, making async code look synchronous.

async Functions

async function greet() {
  return "Hello";
}

// Equivalent to:
function greet() {
  return Promise.resolve("Hello");
}

// Usage
greet().then(msg => console.log(msg));  // "Hello"

Key: async function always returns a promise.

await Keyword

await pauses execution until promise resolves (only works in async functions):

async function fetchData() {
  console.log("Fetching...");

  const user = await fetchUser(1);  // Wait for promise
  console.log("User:", user);

  console.log("Done!");
}

fetchData();

// Output:
// Fetching...
// (wait 1 second)
// User: { id: 1, name: "Alice" }
// Done!

Without async/await (promises):

function fetchData() {
  console.log("Fetching...");

  fetchUser(1).then(user => {
    console.log("User:", user);
    console.log("Done!");
  });
}

Python comparison:

import asyncio

async def fetch_user(user_id):
    await asyncio.sleep(1)
    return {"id": user_id, "name": "Alice"}

async def fetch_data():
    print("Fetching...")
    user = await fetch_user(1)
    print("User:", user)
    print("Done!")

asyncio.run(fetch_data())

Error Handling with try/catch

async function fetchData() {
  try {
    const user = await fetchUser(-1);  // Will fail
    console.log("User:", user);
  } catch (error) {
    console.log("Error:", error);  // "Invalid user ID"
  }
}

fetchData();

Compare to promise .catch():

fetchUser(-1)
  .then(user => console.log("User:", user))
  .catch(error => console.log("Error:", error));

Sequential vs Parallel Execution

Sequential (one after another):

async function sequential() {
  const user1 = await fetchUser(1);  // Wait 1 second
  const user2 = await fetchUser(2);  // Wait 1 second
  console.log(user1, user2);
  // Total: 2 seconds
}

Parallel (simultaneously):

async function parallel() {
  const promise1 = fetchUser(1);  // Start both
  const promise2 = fetchUser(2);

  const user1 = await promise1;   // Wait for first
  const user2 = await promise2;   // Wait for second (already running)

  console.log(user1, user2);
  // Total: 1 second
}

// Or use Promise.all() (better)
async function parallelAll() {
  const [user1, user2] = await Promise.all([
    fetchUser(1),
    fetchUser(2)
  ]);
  console.log(user1, user2);
  // Total: 1 second
}

5. Promise Methods

Promise.all()

Wait for all promises to resolve:

const promise1 = fetchUser(1);
const promise2 = fetchUser(2);
const promise3 = fetchUser(3);

Promise.all([promise1, promise2, promise3])
  .then(users => {
    console.log("All users:", users);
    // [{ id: 1, ... }, { id: 2, ... }, { id: 3, ... }]
  })
  .catch(error => {
    console.log("One failed:", error);
    // If ANY promise fails, entire Promise.all fails
  });

With async/await:

async function getAllUsers() {
  try {
    const users = await Promise.all([
      fetchUser(1),
      fetchUser(2),
      fetchUser(3)
    ]);
    console.log("All users:", users);
  } catch (error) {
    console.log("Error:", error);
  }
}

Promise.race()

Returns first promise to settle (resolve or reject):

const slow = new Promise(resolve => setTimeout(() => resolve("Slow"), 2000));
const fast = new Promise(resolve => setTimeout(() => resolve("Fast"), 500));

Promise.race([slow, fast])
  .then(result => {
    console.log(result);  // "Fast" (wins the race)
  });

Use case: Timeout for slow operations:

function timeout(ms) {
  return new Promise((_, reject) =>
    setTimeout(() => reject("Timeout"), ms)
  );
}

Promise.race([
  fetchUser(1),
  timeout(2000)
])
  .then(user => console.log("User:", user))
  .catch(error => console.log("Error:", error));

Promise.allSettled()

Wait for all promises to settle (doesn't fail if one fails):

Promise.allSettled([
  fetchUser(1),    // Success
  fetchUser(-1),   // Failure
  fetchUser(2)     // Success
])
  .then(results => {
    console.log(results);
    /*
    [
      { status: "fulfilled", value: { id: 1, ... } },
      { status: "rejected", reason: "Invalid user ID" },
      { status: "fulfilled", value: { id: 2, ... } }
    ]
    */
  });

Promise.any()

Returns first successful promise (ignores rejections):

Promise.any([
  Promise.reject("Error 1"),
  Promise.resolve("Success!"),
  Promise.reject("Error 2")
])
  .then(result => {
    console.log(result);  // "Success!"
  })
  .catch(error => {
    // Only if ALL fail
    console.log("All failed:", error);
  });

6. The Event Loop

How JavaScript Handles Async

JavaScript is single-threaded but handles async via the event loop.

Components:

  1. Call Stack - Executes synchronous code
  2. Web APIs - Handle async operations (setTimeout, fetch, etc.)
  3. Callback Queue - Stores callbacks from completed async operations
  4. Event Loop - Moves callbacks from queue to stack when stack is empty

Example:

console.log("1");

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

console.log("3");

// Output:
// 1
// 3
// 2 (even with 0ms delay!)

Why?

  1. console.log("1") executes
  2. setTimeout goes to Web API (even with 0ms)
  3. console.log("3") executes
  4. Call stack is empty
  5. Event loop moves setTimeout callback to stack
  6. console.log("2") executes

Microtasks vs Macrotasks

Microtasks (higher priority):

  • Promises (.then, .catch, .finally)
  • async/await
  • queueMicrotask()

Macrotasks (lower priority):

  • setTimeout
  • setInterval
  • setImmediate (Node.js)
  • I/O operations

Example:

console.log("1");

setTimeout(() => console.log("2"), 0);  // Macrotask

Promise.resolve().then(() => console.log("3"));  // Microtask

console.log("4");

// Output:
// 1
// 4
// 3 (microtask runs before macrotask)
// 2

7. Practical Examples

Example 1: Simulated API with Delay

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

async function fetchWithDelay(url) {
  console.log(`Fetching ${url}...`);
  await delay(1000);  // Simulate network delay
  return { data: `Data from ${url}` };
}

async function getData() {
  const result = await fetchWithDelay("https://api.example.com");
  console.log(result.data);
}

getData();

Example 2: Retry Logic

async function fetchWithRetry(url, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url);
      if (response.ok) {
        return await response.json();
      }
    } catch (error) {
      console.log(`Attempt ${i + 1} failed`);
      if (i === retries - 1) throw error;
    }
  }
}

fetchWithRetry("https://api.example.com/data")
  .then(data => console.log(data))
  .catch(error => console.log("All retries failed"));

Example 3: Loading Multiple Resources

async function loadDashboard() {
  try {
    const [user, posts, comments] = await Promise.all([
      fetch("/api/user").then(r => r.json()),
      fetch("/api/posts").then(r => r.json()),
      fetch("/api/comments").then(r => r.json())
    ]);

    console.log("Dashboard loaded:", { user, posts, comments });
  } catch (error) {
    console.log("Failed to load dashboard:", error);
  }
}

8. Practical Exercises

Exercise 4.7.1: Promise Practice

Create a function that:

  1. Returns a promise
  2. Resolves after random delay (500-2000ms)
  3. Randomly succeeds or fails (50/50)
  4. Handle with .then/.catch

Exercise 4.7.2: Async/Await Conversion

Convert this promise chain to async/await:

fetchUser(1)
  .then(user => fetchPosts(user.id))
  .then(posts => fetchComments(posts[0].id))
  .then(comments => console.log(comments))
  .catch(error => console.log(error));

Exercise 4.7.3: Parallel Execution

Fetch data from 5 URLs simultaneously:

  1. Use Promise.all()
  2. Log results when all complete
  3. Handle errors gracefully
  4. Show loading indicator

Exercise 4.7.4: Race Condition

Create a timeout function that:

  1. Races between actual fetch and timeout
  2. Rejects if timeout wins
  3. Resolves with data if fetch wins

9. Knowledge Check

Question 1: What's the difference between synchronous and asynchronous code?

Show answer Synchronous code executes line by line, blocking until each operation completes. Asynchronous code allows operations to run in background, not blocking execution.

Question 2: What are the three states of a promise?

Show answer Pending (initial), Fulfilled (success), Rejected (failure).

Question 3: What does async/await do?

Show answer Syntactic sugar over promises. `async` makes function return promise. `await` pauses execution until promise resolves, making async code look synchronous.

Question 4: What's the difference between Promise.all() and Promise.race()?

Show answer Promise.all() waits for ALL promises to resolve. Promise.race() returns first promise to settle (resolve or reject).

Question 5: Why does setTimeout with 0ms still run after synchronous code?

Show answer Event loop only processes callback queue after call stack is empty. Even 0ms setTimeout goes to Web API first, then callback queue.

10. Key Takeaways

  • JavaScript is single-threaded but handles async via event loop
  • Callbacks are functions passed as arguments, executed later
  • Promises represent eventual result of async operation (pending/fulfilled/rejected)
  • async/await is modern syntax for promises (cleaner, more readable)
  • await only works inside async functions
  • Use try/catch for error handling in async/await
  • Promise.all() for parallel execution (all must succeed)
  • Promise.race() for first to complete
  • Microtasks (promises) run before macrotasks (setTimeout)
  • Always handle errors in async code

11. Further Resources

Documentation:


Next Steps

Outstanding! You now understand asynchronous JavaScript and can write clean async code.

In Lesson 4.8: Fetch API & AJAX, you'll learn to make HTTP requests to fetch data from servers and build data-driven applications.

Next: Lesson 4.8 - Fetch API & AJAX →