Synchronous vs. Asynchronous

By default, JavaScript is synchronous. It executes one line of code at a time, in order. If a line of code takes a long time to run (e.g., a complex calculation or a network request), it blocks everything else from happening.

Asynchronous code allows the program to start a long-running task and continue with other tasks without waiting for it to finish. When the long task is complete, the program is notified and can handle the result. This is crucial for a smooth user experience on the web.

The Old Way: Callbacks Initially, async operations were handled with callback functions. This often led to nested, hard-to-read code known as "callback hell."

A Better Way: Promises

A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation. It exists in one of three states:

  • Pending: The initial state; the operation hasn't completed yet.
  • Fulfilled: The operation completed successfully, and the promise has a resulting value.
  • Rejected: The operation failed, and the promise has a reason for the failure.

You handle the outcome of a promise using the .then() and .catch() methods.

Code Snippet: Using a Promise (fetch) The modern fetch API for making network requests returns a Promise.

JavaScript


console.log("Starting data fetch...");

fetch('https://api.example.com/data')
  .then(response => {
    // This .then() handles a fulfilled fetch promise
    if (!response.ok) {
        throw new Error('Network response was not ok');
    }
    return response.json(); // .json() also returns a promise!
  })
  .then(data => {
    // This .then() handles the fulfilled .json() promise
    console.log("Data received:", data);
  })
  .catch(error => {
    // This .catch() handles any rejection in the promise chain
    console.error("Fetch error:", error);
  });

console.log("This message logs immediately, without waiting for the fetch.");

The Modern Way: async/await

async/await is modern syntactic sugar built on top of Promises. It lets you write asynchronous code that looks and behaves like synchronous code, making it much easier to read and reason about.

  • async: The async keyword is placed before a function declaration to turn it into an async function. An async function always implicitly returns a promise.
  • await: The await keyword can only be used inside an async function. It pauses the function's execution and waits for a Promise to be resolved or rejected before continuing.

Code Snippet: Refactoring fetch with async/await Let's rewrite the previous example. Note the use of try...catch for error handling.

JavaScript


async function fetchData() {
  console.log("Starting data fetch...");
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
        throw new Error('Network response was not ok');
    }
    const data = await response.json();
    console.log("Data received:", data);
  } catch (error) {
    console.error("Fetch error:", error);
  }
  console.log("This message logs after the fetch is complete (or has failed).");
}

fetchData();