DNA⚡ JavaScriptCallbacks, Promises & Async/Await
ðŸĢHatchlingJavaScriptAsyncPromisesFundamentals

Callbacks, Promises & Async/Await

The evolution from callback hell to promises to async/await isn't just syntax sugar — each model has different error handling, composition, and cancellation characteristics.

Callbacks, Promises & Async/Await

Understanding the full async evolution is a core interview signal. Senior engineers don't just prefer async/await for readability — they understand the error propagation model, know when promises compose better, and can articulate callback limitations beyond "callback hell."

Callbacks

A callback is a function passed as an argument to be called later:

function fetchData(url, callback) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.onload = () => callback(null, JSON.parse(xhr.responseText));
  xhr.onerror = () => callback(new Error('Request failed'));
  xhr.send();
}
 
fetchData('/api/user', (err, user) => {
  if (err) {
    console.error(err);
    return;
  }
  console.log(user);
});

The Node.js Error-First Convention

fs.readFile('data.json', 'utf-8', (err, data) => {
  if (err) {
    // handle error
    return;
  }
  // use data
});

Callback Hell (Pyramid of Doom)

Sequential async operations create deeply nested code:

getUser(userId, (err, user) => {
  if (err) return handleError(err);
  getOrders(user.id, (err, orders) => {
    if (err) return handleError(err);
    getOrderDetails(orders[0].id, (err, details) => {
      if (err) return handleError(err);
      getShippingStatus(details.trackingId, (err, status) => {
        if (err) return handleError(err);
        render(status);
      });
    });
  });
});

Problems beyond nesting:

  • Error handling duplicated at every level
  • Impossible to run operations in parallel and wait for all
  • No built-in cancellation
  • Inversion of control — you trust the callee to call your callback correctly

Promises

A Promise represents a future value with three states: pending, fulfilled, rejected:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = Math.random() > 0.5;
    success ? resolve('data') : reject(new Error('failed'));
  }, 1000);
});
 
promise
  .then(data => console.log(data))
  .catch(err => console.error(err))
  .finally(() => console.log('done'));

Promise Chaining

Each .then() returns a new promise, enabling flat sequential flows:

getUser(userId)
  .then(user => getOrders(user.id))
  .then(orders => getOrderDetails(orders[0].id))
  .then(details => getShippingStatus(details.trackingId))
  .then(status => render(status))
  .catch(err => handleError(err)); // One catch handles ALL errors

Parallel Execution

const [user, posts, notifications] = await Promise.all([
  fetchUser(id),
  fetchPosts(id),
  fetchNotifications(id),
]);

Promise Static Methods

MethodResolves WhenRejects When
Promise.allAll fulfillAny rejects
Promise.raceFirst settlesFirst settles
Promise.anyFirst fulfillsAll reject
Promise.allSettledAll settleNever
// Resilient — get whatever succeeds
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);
const successes = results.filter(r => r.status === 'fulfilled').map(r => r.value);
const failures = results.filter(r => r.status === 'rejected').map(r => r.reason);

Microtask Scheduling

Promise callbacks (.then, .catch, .finally) run as microtasks — before the next macrotask:

console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
 
// Output: 1, 4, 3, 2

Async/Await

Syntactic sugar over promises that makes async code read like synchronous code:

async function loadDashboard(userId) {
  try {
    const user = await getUser(userId);
    const orders = await getOrders(user.id);
    const details = await getOrderDetails(orders[0].id);
    const status = await getShippingStatus(details.trackingId);
    render(status);
  } catch (err) {
    handleError(err);
  }
}

async Function Return Value

An async function always returns a promise:

async function getValue() { return 42; }
getValue(); // Promise<42>
 
async function throwError() { throw new Error('oops'); }
throwError(); // Promise.reject(Error('oops'))

Sequential vs Parallel

// SEQUENTIAL — each waits for the previous (slow)
const user = await fetchUser(id);
const posts = await fetchPosts(id);
const comments = await fetchComments(id);
 
// PARALLEL — all fire simultaneously (fast)
const [user, posts, comments] = await Promise.all([
  fetchUser(id),
  fetchPosts(id),
  fetchComments(id),
]);

Error Handling Patterns

// try/catch (most common)
async function load() {
  try {
    const data = await fetchData();
    return data;
  } catch (err) {
    return fallbackData;
  }
}
 
// .catch() on the promise (inline)
const data = await fetchData().catch(() => fallbackData);
 
// Wrapper pattern (Go-style)
async function to(promise) {
  try {
    const data = await promise;
    return [null, data];
  } catch (err) {
    return [err, null];
  }
}
 
const [err, user] = await to(fetchUser(id));
if (err) return handleError(err);

Top-Level Await

ES2022 allows await at the module top level:

// module.js
const config = await fetch('/config.json').then(r => r.json());
export default config;

Comparison Table

FeatureCallbacksPromisesAsync/Await
ReadabilityPoor (nested)Good (chained)Best (sequential)
Error handlingManual at every level.catch() catches alltry/catch
Parallel opsManual (counters)Promise.allPromise.all + await
CancellationNot built-inNot built-in (use AbortController)Not built-in
DebuggingFragmented stack tracesBetter stack tracesBest stack traces
Return valueNone (via callback)Promise objectPromise (implicit)

Common Pitfalls

Forgetting to Await

// BUG: fires and forgets — errors are swallowed
async function save() {
  updateDatabase(data); // Missing await!
}
 
// FIX
async function save() {
  await updateDatabase(data);
}

Await in Loops

// SEQUENTIAL — each iteration waits (often a bug)
for (const url of urls) {
  const data = await fetch(url); // One at a time
}
 
// PARALLEL — all fire at once
const results = await Promise.all(urls.map(url => fetch(url)));

Unhandled Rejections

// This promise rejection will crash Node.js (unhandled)
async function risky() {
  throw new Error('oops');
}
risky(); // No .catch(), no try/catch wrapping the caller
 
// Always handle
risky().catch(handleError);

Interview Signal

Senior candidates demonstrate:

  1. Evolution understanding — Why promises replaced callbacks (inversion of control, error propagation), why async/await improved on promises (readability, debugging)
  2. Microtask scheduling — Promise callbacks are microtasks, setTimeout is a macrotask, execution order
  3. Parallel vs sequential — Knows await in a loop is sequential, Promise.all is parallel
  4. Error propagation — One .catch handles a chain, try/catch in async/await, unhandled rejection consequences
  5. Practical patterns — Promise.allSettled for resilience, Go-style to() wrapper, AbortController for cancellation