Asynchronous programming in JavaScript allows tasks to run without blocking the main thread. This is essential for network requests, timers, file I/O, and any operation that would freeze the UI if executed synchronously.

Synchronous vs Asynchronous

  console.log('Start');

setTimeout(() => {
    console.log('Async task done');
}, 1000);

console.log('End');
// Output: Start, End, Async task done (after 1 second)
  

JavaScript is single-threaded — one call stack. Async operations delegate to the browser (Web APIs) and return via the callback queue processed by the event loop.

Callback Functions

A callback is a function passed to another function, invoked when an async operation completes:

  function fetchData(callback) {
    setTimeout(() => {
        console.log('Data fetched');
        callback(null, { id: 1, name: 'Alice' });
    }, 1000);
}

function displayData(err, data) {
    if (err) return console.error(err);
    console.log('Display:', data);
}

fetchData(displayData);
  

Callback Hell

Nested callbacks become hard to read and error-prune:

  getUser(1, (err, user) => {
    getOrders(user.id, (err, orders) => {
        getOrderDetails(orders[0].id, (err, details) => {
            // deeply nested...
        });
    });
});
  

Promises and async/await solve this.

Promises

A Promise represents a future value — pending, fulfilled, or rejected:

  const promise = new Promise((resolve, reject) => {
    const success = true;
    if (success) {
        resolve('Operation successful');
    } else {
        reject(new Error('Operation failed'));
    }
});

promise
    .then(message => console.log(message))
    .catch(error => console.error(error.message))
    .finally(() => console.log('Cleanup always runs'));
  

Chaining Promises

  fetchUser(1)
    .then(user => fetchOrders(user.id))
    .then(orders => fetchDetails(orders[0].id))
    .then(details => console.log(details))
    .catch(err => console.error('Any step failed:', err));
  

Return a value or Promise from .then() to chain. Throw or return rejected Promise to trigger .catch().

Promise Utilities

  // All must succeed
Promise.all([fetchUser(), fetchPosts()])
    .then(([user, posts]) => console.log(user, posts))
    .catch(err => console.error('One failed:', err));

// All settle — never rejects
Promise.allSettled([fetchA(), fetchB()])
    .then(results => {
        results.forEach(r => {
            if (r.status === 'fulfilled') console.log(r.value);
            else console.log(r.reason);
        });
    });

// First to resolve wins
Promise.race([fetchFromCDN1(), fetchFromCDN2()])
    .then(data => console.log('Fastest:', data));

// First fulfilled (ignores rejections until all fail)
Promise.any([fetchMirror1(), fetchMirror2()])
    .then(data => console.log(data));
  

Async/Await

Syntax sugar over Promises — makes async code read like synchronous:

  function fetchData() {
    return new Promise((resolve) => {
        setTimeout(() => resolve('Data fetched'), 1000);
    });
}

async function displayData() {
    const data = await fetchData();
    console.log(data);
}

displayData();
  

async functions always return a Promise. await pauses within the async function until Promise settles.

Error Handling

  async function loadUser(id) {
    try {
        const response = await fetch(`/api/users/${id}`);
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        return await response.json();
    } catch (error) {
        console.error('Failed to load user:', error.message);
        throw error; // re-throw for caller to handle
    }
}
  

Prefer try/catch around await — cleaner than .catch() chains in async functions.

Sequential vs Parallel Execution

  async function sequential() {
    const user = await fetchUser();    // wait
    const orders = await fetchOrders(user.id); // then wait
    return orders;
}

async function parallel() {
    const [user, posts] = await Promise.all([
        fetchUser(),
        fetchPosts()
    ]);
    return { user, posts };
}
  

Use parallel when tasks are independent — often 2–10× faster than sequential.

Fetch API Example

  async function getJSON(url) {
    const response = await fetch(url, {
        method: 'GET',
        headers: { 'Accept': 'application/json' }
    });
    if (!response.ok) throw new Error(response.statusText);
    return response.json();
}

getJSON('/api/data')
    .then(data => console.log(data))
    .catch(err => console.error(err));
  

Common Patterns

Timeout Wrapper

  function withTimeout(promise, ms) {
    const timeout = new Promise((_, reject) =>
        setTimeout(() => reject(new Error('Timeout')), ms)
    );
    return Promise.race([promise, timeout]);
}

await withTimeout(fetch('/api/slow'), 5000);
  

Retry with Backoff

  async function fetchWithRetry(url, retries = 3) {
    for (let i = 0; i < retries; i++) {
        try {
            return await fetch(url);
        } catch (err) {
            if (i === retries - 1) throw err;
            await new Promise(r => setTimeout(r, 1000 * (i + 1)));
        }
    }
}
  

Best Practices

  1. Always handle errors — unhandled Promise rejections crash Node and warn in browsers
  2. Use Promise.all for independent tasks — don’t await in a loop unnecessarily
  3. Avoid mixing callbacks and Promises — wrap callbacks with new Promise
  4. Don’t await in loops when parallel is possible — use Promise.all(items.map(...))
  5. Understand the event loop — see Event Loop chapter

Troubleshooting

await syntax error

  • await only valid inside async functions (or top-level in modules)

Promise never resolves

  • Missing resolve()/reject() call
  • Forgot to return Promise from function

Race conditions

  • Multiple async updates to same state — use abort controllers or sequential queue

Unhandled rejection

  • Add .catch() or wrap in try/catch

Async JavaScript powers modern web apps — master Promises and async/await for clean, maintainable asynchronous code.