JavaScript logoJavaScriptINTERMEDIATE

Async JavaScript

Async JavaScript cheat sheet covering Promises, async/await, error handling, Promise.all, race conditions, and concurrency patterns.

8 min read
javascriptasyncpromisesasync-awaitcallbacks

Sign in to mark items as known and track your progress.

Sign in

Callbacks

Basic Callbacks

Traditional pattern for handling asynchronous operations with callback functions

javascript
// Simple callback
function getData(callback) {
  setTimeout(() => {
    callback('Data loaded');
  }, 1000);
}

getData(result => console.log(result));

// Error-first callback (Node.js pattern)
function readFile(path, callback) {
  fs.readFile(path, (err, data) => {
    if (err) callback(err, null);
    else callback(null, data);
  });
}
💡 Callbacks are functions passed as arguments to be executed later
⚠️ Can lead to "callback hell" with deep nesting
📌 Error-first convention: first parameter is for errors
🔄 Still used in many Node.js APIs and older libraries

Callback Hell

The problem of deeply nested callbacks and how to avoid it with modern patterns

javascript
// Callback hell (pyramid of doom)
getData((a) => {
  getMoreData(a, (b) => {
    getMoreData(b, (c) => {
      getMoreData(c, (d) => {
        console.log(d);
      });
    });
  });
});

// Better: Named functions
function step1(callback) { getData(callback); }
function step2(a, callback) { getMoreData(a, callback); }
function step3(b, callback) { getMoreData(b, callback); }
⚠️ Deeply nested callbacks create pyramid of doom
💡 Each level depends on the previous, making it hard to read
✅ Solved by promises and async/await patterns
🔄 Named functions can help flatten callback structures

Timers & Event Loop

Timer Functions

Schedule code execution after delays or at regular intervals using built-in timer functions

javascript
// setTimeout
const timeoutId = setTimeout(() => {
  console.log('Executed after 2 seconds');
}, 2000);

// setInterval
const intervalId = setInterval(() => {
  console.log('Every second');
}, 1000);

// Clear timers
clearTimeout(timeoutId);
clearInterval(intervalId);

// setImmediate (Node.js)
setImmediate(() => {
  console.log('Execute after I/O');
});
💡 setTimeout executes once after a delay, setInterval repeats
⚠️ Timers are not guaranteed to execute at exact times
🔄 Always clear intervals and timeouts when no longer needed
⚡ Use setTimeout with 0ms to defer execution to next tick

Event Loop

Understand how JavaScript handles asynchronous operations through the event loop mechanism

javascript
// Event loop phases
console.log('1'); // Call stack

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

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

console.log('4'); // Call stack

// Output: 1, 4, 3, 2
💡 JavaScript is single-threaded but uses an event loop for async operations
⚡ Microtasks (promises) have priority over macrotasks (setTimeout)
📌 Understanding the event loop helps debug timing issues
🔄 Call stack must be empty before event loop processes callbacks

Promises

Creating Promises

Create a new Promise that resolves or rejects based on asynchronous operations

javascript
// Create a promise
const promise = new Promise((resolve, reject) => {
  if (success) resolve(value);
  else reject(error);
});

// Using promises
promise
  .then(result => console.log(result))
  .catch(error => console.error(error))
  .finally(() => console.log('Done'));

// Promise from callback
function promisify(fn) {
  return (...args) => new Promise((resolve, reject) => {
    fn(...args, (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}
💡 A Promise is an object representing a future value that may not be available yet
⚡ Promises have three states: pending, fulfilled (resolved), or rejected
✅ Better than callbacks for handling async operations and avoiding callback hell
🔄 Once settled (fulfilled or rejected), a promise state cannot change

Promise Combinators

Utility methods for coordinating multiple promises with different execution strategies

javascript
// Promise.all - all must succeed
Promise.all([p1, p2, p3])
  .then(results => console.log(results));

// Promise.race - first to settle wins
Promise.race([p1, p2, p3])
  .then(winner => console.log(winner));

// Promise.allSettled - wait for all
Promise.allSettled([p1, p2, p3])
  .then(results => results.forEach(r => 
    console.log(r.status, r.value || r.reason)
  ));

// Promise.any - first success wins
Promise.any([p1, p2, p3])
  .then(first => console.log(first));
💡 Promise.all fails fast - rejects if any promise rejects
✅ Promise.allSettled waits for all regardless of outcome
⚡ Promise.race returns first to settle (resolve or reject)
🔄 Promise.any returns first to resolve, ignores rejections

Async/Await

Basic Async/Await

Modern syntax for writing asynchronous code that looks and behaves like synchronous code

javascript
// Async function
async function getData() {
  const response = await fetch('/api/data');
  const data = await response.json();
  return data;
}

// Error handling
async function safeGet() {
  try {
    const data = await getData();
    console.log(data);
  } catch (error) {
    console.error('Failed:', error);
  }
}

// Parallel execution
async function parallel() {
  const [a, b] = await Promise.all([
    fetch('/api/a'),
    fetch('/api/b')
  ]);
}
💡 Async/await is syntactic sugar built on top of promises
⚡ Makes asynchronous code look and behave like synchronous code
⚠️ Can only use await inside async functions
✅ Easier to read and debug than promise chains

Advanced Async Patterns

Complex patterns for handling concurrency, throttling, and async flow control

javascript
// Async iterator
async function* asyncGenerator() {
  yield await fetch('/page/1');
  yield await fetch('/page/2');
  yield await fetch('/page/3');
}

for await (const page of asyncGenerator()) {
  console.log(await page.json());
}

// Retry pattern
async function retry(fn, retries = 3) {
  try {
    return await fn();
  } catch (err) {
    if (retries > 0) {
      await delay(1000);
      return retry(fn, retries - 1);
    }
    throw err;
  }
}
💡 Throttling limits function calls to a maximum rate
⚡ Debouncing delays execution until after calls stop
✅ Queues help manage concurrent operation limits
🔄 Async iterators enable processing of streaming data

await using & Promise.withResolvers (ES2024)

Explicit resource management with `using` + the deferred-promise helper

javascript
// await using — auto-dispose async resources when the block ends
class DbConnection {
  async [Symbol.asyncDispose]() { await this.close() }
}

async function getUser(id) {
  await using conn = await pool.acquire()   // released no matter how we exit
  return conn.query('SELECT * FROM users WHERE id = $1', [id])
}

// Promise.withResolvers — get a promise + its resolve/reject up front
function once(emitter, event) {
  const { promise, resolve, reject } = Promise.withResolvers()
  emitter.once(event, resolve)
  emitter.once('error', reject)
  return promise
}
💡 `await using x = ...` is the cleanup-guaranteed alternative to try/finally
📌 Implement Symbol.dispose / Symbol.asyncDispose to make any class disposable
⚡ Multiple disposables release in REVERSE order — predictable teardown
🎯 Promise.withResolvers replaces the hand-rolled "deferred" pattern
es2024resource-managementmodern

Abort & Error Handling

AbortController

Cancel ongoing asynchronous operations like fetch requests or long-running tasks

javascript
// Basic abort
const controller = new AbortController();
const signal = controller.signal;

fetch('/api/data', { signal })
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('Aborted');
    }
  });

// Abort after timeout
setTimeout(() => controller.abort(), 5000);

// Multiple operations
const controller = new AbortController();
Promise.all([
  fetch('/api/1', { signal: controller.signal }),
  fetch('/api/2', { signal: controller.signal })
]);
controller.abort(); // Cancels all
💡 Allows you to cancel fetch requests and other async operations
⚡ Prevents unnecessary work when user navigates away
✅ Essential for cleaning up long-running operations
🔄 Can be reused by creating new controller instances

Error Handling Strategies

Best practices and patterns for handling errors in asynchronous JavaScript code

javascript
// Try/catch with async/await
async function safe() {
  try {
    return await riskyOperation();
  } catch (error) {
    logger.error(error);
    return defaultValue;
  }
}

// Promise error handling
promise
  .then(handleSuccess)
  .catch(handleError)
  .finally(cleanup);

// Global error handlers
window.addEventListener('unhandledrejection', e => {
  console.error('Unhandled promise:', e.reason);
});
💡 Always handle promise rejections to avoid unhandled errors
⚠️ Async errors don't bubble up like synchronous errors
✅ Use try-catch with async/await for cleaner error handling
🔄 Consider implementing retry logic for transient failures