DevToolBoxFREE
Blog

JavaScript Promises and Async/Await: Complete Guide

16 min readby DevToolBox

TL;DR — Quick Summary

Promises are the foundation of asynchronous JavaScript. A Promise has three states: pending, fulfilled, and rejected. Use async/await for readable async code; Promise.all for parallel execution; try/catch or .catch() for error handling; Promise.race for timeouts; and Promise.allSettled when you need all results even if some fail. This guide covers everything from basics to advanced patterns with real code examples.

Key Takeaways

  • A Promise is a single-use container for an async result — its state is immutable once settled
  • async/await is syntactic sugar over Promises — they are functionally identical
  • Promise.all fails fast; Promise.allSettled waits for all and never rejects
  • Errors must be explicitly caught with .catch() or try/catch — or they become unhandled rejections
  • Parallel execution with Promise.all is dramatically faster than sequential awaits
  • Promise.race enables timeout wrappers and fastest-source patterns
  • Avoid forgetting await, swallowing errors, and nesting Promises (the Promise equivalent of callback hell)

1. Promise Basics: States and Lifecycle

Before Promises, JavaScript handled asynchronous operations through nested callback functions — a pattern notorious for producing deeply indented, hard-to-maintain "callback hell." ES6 (2015) introduced Promises as a structured way to represent the eventual completion (or failure) of an asynchronous operation and its resulting value.

A Promise object represents an asynchronous operation that has exactly three mutually exclusive states:

  • Pending — The initial state. The operation has not yet completed.
  • Fulfilled — The operation completed successfully and the Promise has a resolved value.
  • Rejected — The operation failed and the Promise has a rejection reason (typically an Error object).

Once a Promise transitions from pending to either fulfilled or rejected, it is settled — its state and value are permanently fixed. This immutability is a core guarantee of the Promise specification.

// The three Promise states
const pending   = new Promise(() => {});           // Never settles
const fulfilled = Promise.resolve('success');      // Immediately fulfilled
const rejected  = Promise.reject(new Error('fail')); // Immediately rejected

// Handle results with .then() and .catch()
fulfilled
  .then(value  => console.log('Value:', value))   // Value: success
  .catch(error => console.error('Error:', error));

rejected
  .then(value  => console.log('Never runs'))
  .catch(error => console.error('Caught:', error.message)); // Caught: fail

// .finally() runs regardless of outcome
fetch('/api/data')
  .then(res  => res.json())
  .catch(err => ({ error: err.message }))
  .finally(() => hideLoadingSpinner()); // Always executes

.then(), .catch(), and .finally()

The three core Promise handler methods cover every outcome scenario. Each returns a new Promise, enabling the fluent chaining pattern:

// .then(onFulfilled, onRejected) — both params are optional
promise.then(
  value => console.log('Fulfilled with:', value),
  error => console.log('Rejected with:', error)   // rarely used
);

// Best practice: use .catch() instead of the second .then() arg
// .catch(onRejected) is shorthand for .then(undefined, onRejected)
fetch('/api/users')
  .then(response => {
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  })
  .then(users => {
    renderUserList(users);
    return users.length;  // Pass a value to the next .then()
  })
  .then(count => console.log(`Rendered ${count} users`))
  .catch(error => {
    // Catches ALL errors from the entire chain above
    console.error('Failed to load users:', error.message);
    return [];   // Recover: return default value to continue the chain
  })
  .finally(() => {
    // Cleanup: runs whether fulfilled or rejected
    setLoading(false);
  });

2. Creating Promises: new Promise(), resolve, and reject

You create a Promise with the new Promise(executor) constructor. The executor function receives two parameters: resolve (call when the operation succeeds) and reject (call when it fails). The executor runs synchronously — the Promise body executes immediately.

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

await delay(1000);  // Pauses for 1 second

// Promise with a value
function delayedValue(value, ms) {
  return new Promise(resolve =>
    setTimeout(() => resolve(value), ms)
  );
}
const result = await delayedValue('hello', 500);  // 'hello' after 500ms

// Wrapping callback-based APIs
function readFileAsync(path, encoding = 'utf8') {
  return new Promise((resolve, reject) => {
    require('fs').readFile(path, encoding, (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

// Simulating an async operation with random failure
function unstableRequest() {
  return new Promise((resolve, reject) => {
    const willSucceed = Math.random() > 0.3;
    setTimeout(() => {
      if (willSucceed) resolve({ status: 'ok', data: [1, 2, 3] });
      else reject(new Error('Request failed: server unavailable'));
    }, Math.random() * 1000);
  });
}

// Shorthand static methods
const immediate = Promise.resolve({ id: 1, name: 'Alice' });
const failure   = Promise.reject(new TypeError('Expected string'));
// Always catch rejected promises or they become unhandled rejections
failure.catch(() => {});

3. Promise Chaining: Error Propagation and Return Values

Because .then() always returns a new Promise, you can chain operations in a flat, readable sequence. A value returned from a .then() handler becomes the resolved value of the next Promise in the chain. An error thrown in any step propagates through the chain to the nearest .catch().

// Correct: flat chain (not nested)
fetchUser(userId)
  .then(user => fetchUserPosts(user.id))     // Returns a Promise
  .then(posts => fetchComments(posts[0].id)) // Waits for posts first
  .then(comments => renderComments(comments))
  .catch(err => showErrorMessage(err));

// Error propagation: errors skip .then() handlers
Promise.resolve('start')
  .then(val => { throw new Error('step 2 failed'); })
  .then(val => console.log('skipped'))
  .then(val => console.log('also skipped'))
  .catch(err => console.log('Caught:', err.message)); // Caught: step 2 failed

// Recovering in .catch() — the chain continues after catching
fetchPrimary()
  .catch(err => {
    console.warn('Primary source failed, using backup:', err.message);
    return fetchBackup();  // Return backup Promise — chain resumes
  })
  .then(data => processData(data)); // Runs with backup data

// Passing multiple values through the chain
fetchOrderById(orderId)
  .then(order => {
    return Promise.all([
      Promise.resolve(order),          // Pass through the original
      fetchCustomer(order.customerId), // Fetch related data in parallel
    ]);
  })
  .then(([order, customer]) => generateInvoice(order, customer))
  .then(invoice => sendEmail(invoice))
  .catch(err => logError('Invoice generation failed', err));

4. async/await: The Modern Async Syntax

Introduced in ES2017, async/await is syntactic sugar that makes Promise-based code read like synchronous code. An async function always returns a Promise. The await keyword pauses execution of the async function until the awaited Promise settles, then resumes with the resolved value.

// async function always returns a Promise
async function greet(name) {
  return `Hello, ${name}!`;  // Auto-wrapped in Promise.resolve(...)
}
greet('Alice').then(console.log); // Hello, Alice!

// await pauses execution until the Promise settles
async function loadDashboard(userId) {
  const user     = await fetchUser(userId);      // Waits here
  const settings = await fetchSettings(user.id); // Then waits here
  const stats    = await fetchStats(user.id);    // Then waits here
  return { user, settings, stats };
}

// Error handling with try/catch
async function safeLoadDashboard(userId) {
  try {
    const user = await fetchUser(userId);
    const data = await fetchUserData(user.id);
    return renderDashboard(user, data);
  } catch (error) {
    if (error.status === 404) {
      return renderNotFound();
    }
    console.error('Dashboard load failed:', error);
    throw error;  // Re-throw errors you cannot handle here
  } finally {
    hideLoadingIndicator();  // Always runs
  }
}

// Top-level await (ES2022, in ES Modules)
const config = await loadConfig('./app.config.json');
console.log('App version:', config.version);

Sequential vs. Parallel Execution — The Critical Performance Difference

The most common async/await performance mistake is using sequential awaits for independent operations. Each sequential await adds its wait time to the total, while parallel execution only takes as long as the slowest operation.

// SLOW: Sequential — total time = 300 + 200 + 400 = 900ms
async function slowApproach() {
  const users    = await fetchUsers();     // Waits 300ms
  const products = await fetchProducts();  // Then waits 200ms more
  const orders   = await fetchOrders();    // Then waits 400ms more
  return { users, products, orders };
}

// FAST: Parallel — total time ≈ max(300, 200, 400) = 400ms
async function fastApproach() {
  const [users, products, orders] = await Promise.all([
    fetchUsers(),
    fetchProducts(),
    fetchOrders(),
  ]);
  return { users, products, orders };
}

// Parallel array processing (not sequential!)
async function processAllItems(items) {
  // Correct: start all at once, wait for all to finish
  return Promise.all(items.map(item => processItem(item)));
}

// Common mistake: for...of with await is sequential
async function sequentialByMistake(items) {
  const results = [];
  for (const item of items) {
    results.push(await processItem(item)); // Processes one at a time!
  }
  return results;
}

5. Promise Combinators: all, allSettled, race, any

JavaScript provides four static combinator methods for coordinating multiple Promises, each with a different resolution strategy suited to different use cases:

MethodResolves whenRejects whenBest use case
Promise.allALL fulfillANY single rejects (fail-fast)All results required; one failure invalidates all
Promise.allSettledALL settle (any status)Never rejectsBatch ops; need all statuses including failures
Promise.raceFIRST settles (any status)FIRST rejectsTimeout wrapper; first-response wins
Promise.anyFIRST fulfillsALL reject (AggregateError)Multiple fallback sources; first success wins
// Promise.all — all or nothing
try {
  const [user, posts, friends] = await Promise.all([
    fetchUser(id),
    fetchPosts(id),
    fetchFriends(id),
  ]);
  renderProfile(user, posts, friends);
} catch (error) {
  console.error('Profile load failed:', error.message);
}

// Promise.allSettled — batch operations with partial failure tolerance
const imageUploads = urls.map(url => uploadImage(url));
const outcomes = await Promise.allSettled(imageUploads);

let succeeded = 0, failed = 0;
outcomes.forEach((outcome, index) => {
  if (outcome.status === 'fulfilled') {
    console.log(`Image ${index}: uploaded`);
    succeeded++;
  } else {
    console.warn(`Image ${index}: failed — ${outcome.reason.message}`);
    failed++;
  }
});
console.log(`${succeeded} uploaded, ${failed} failed`);

// Promise.race — take the first settled result (including rejections)
const firstCache = await Promise.race([
  fetchFromRedis(key),
  fetchFromMemcached(key),
]);

// Promise.any — first success, ignores individual rejections
try {
  const data = await Promise.any([
    fetchFromPrimaryDB(),
    fetchFromReplicaDB(),
    fetchFromCacheFallback(),
  ]);
  console.log('Got data from fastest source:', data);
} catch (aggregateError) {
  // Thrown only when ALL promises reject
  aggregateError.errors.forEach((err, i) =>
    console.error(`Source ${i + 1}: ${err.message}`)
  );
}

6. Error Handling Patterns

Robust error handling is where many async codebases fall short. Unhandled Promise rejections crash Node.js processes (since v15) and trigger warnings in browsers. Here are the essential patterns for handling async errors correctly.

// Custom error classes for better categorization
class NetworkError extends Error {
  constructor(message, statusCode, endpoint) {
    super(message);
    this.name = 'NetworkError';
    this.statusCode = statusCode;
    this.endpoint = endpoint;
  }
}

class ValidationError extends Error {
  constructor(message, field, value) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
    this.value = value;
  }
}

class NotFoundError extends Error {
  constructor(resource, id) {
    super(`${resource} with id ${id} not found`);
    this.name = 'NotFoundError';
    this.resource = resource;
    this.id = id;
  }
}

// Differentiated error handling with instanceof
async function updateUserProfile(userId, data) {
  try {
    validateProfileData(data);           // Throws ValidationError
    const user = await fetchUser(userId); // Throws NetworkError or NotFoundError
    return await saveUser({ ...user, ...data });
  } catch (error) {
    if (error instanceof ValidationError) {
      return { success: false, error: 'validation', field: error.field };
    }
    if (error instanceof NotFoundError) {
      return { success: false, error: 'not_found' };
    }
    if (error instanceof NetworkError && error.statusCode >= 500) {
      throw error;  // Retry-able — let the caller handle
    }
    throw error;  // Unknown error — re-throw for upper layers
  }
}

// Global handlers (last resort)
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection:', reason);
  process.exit(1); // Recommended in Node.js 15+
});

window.addEventListener('unhandledrejection', event => {
  console.error('Unhandled Promise rejection:', event.reason);
  reportToMonitoring(event.reason);
  event.preventDefault();
});

7. Advanced Patterns

Promise.withResolvers() — ES2024

Promise.withResolvers() (Node.js 22+, Chrome 119+) returns an object containing a new Promise together with its resolve and reject functions — eliminating the need to capture them through the constructor callback.

// Old "deferred" pattern — verbose variable capture
let externalResolve, externalReject;
const deferred = new Promise((res, rej) => {
  externalResolve = res;
  externalReject  = rej;
});

// New: Promise.withResolvers() — clean and explicit
const { promise, resolve, reject } = Promise.withResolvers();

// Practical use: event-to-promise bridge
function waitForEvent(emitter, event, errorEvent = 'error') {
  const { promise, resolve, reject } = Promise.withResolvers();

  const onSuccess = data => { emitter.off(errorEvent, onError); resolve(data); };
  const onError   = err  => { emitter.off(event, onSuccess);    reject(err);  };

  emitter.once(event, onSuccess);
  emitter.once(errorEvent, onError);
  return promise;
}

const socket = createWebSocket('wss://api.example.com');
await waitForEvent(socket, 'open');
console.log('WebSocket connected!');

// Polyfill for older environments
if (!Promise.withResolvers) {
  Promise.withResolvers = function() {
    let resolve, reject;
    const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
    return { promise, resolve, reject };
  };
}

Promisifying Callback-Based APIs

const { promisify } = require('util');
const fs  = require('fs');
const dns = require('dns');

// Node.js util.promisify for standard (err, result) callbacks
const readFile  = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
const dnsLookup = promisify(dns.lookup);

const config = JSON.parse(await readFile('./config.json', 'utf8'));
const { address } = await dnsLookup('example.com');

// Manual promisify utility for any callback-style function
function promisifyFn(fn, context = null) {
  return function promisified(...args) {
    return new Promise((resolve, reject) => {
      fn.call(context, ...args, (err, ...results) => {
        if (err) return reject(err);
        resolve(results.length <= 1 ? results[0] : results);
      });
    });
  };
}

// Modern Node.js: use fs/promises (already promisified)
import { readFile, writeFile, readdir } from 'fs/promises';

const content  = await readFile('./data.txt', 'utf8');
const fileList = await readdir('./src');
await writeFile('./output.txt', content.toUpperCase());

Bounded Concurrency: Processing Large Datasets in Parallel

// Process items with a maximum concurrency limit
async function withBoundedConcurrency(items, processItem, concurrency = 5) {
  const results = new Array(items.length);
  const inFlight = new Set();

  for (let i = 0; i < items.length; i++) {
    const index = i;
    const promise = processItem(items[i]).then(result => {
      results[index] = result;
      inFlight.delete(promise);
    });
    inFlight.add(promise);

    if (inFlight.size >= concurrency) {
      await Promise.race(inFlight);  // Wait for a slot to open
    }
  }

  await Promise.all(inFlight);  // Wait for remaining in-flight
  return results;
}

// Usage: process 1000 items, 5 at a time
const userIds = Array.from({ length: 1000 }, (_, i) => i + 1);
const profiles = await withBoundedConcurrency(
  userIds,
  id => fetch(`/api/users/${id}`).then(r => r.json()),
  5  // Max 5 concurrent requests
);

8. Real-World Examples

Fetch with Retry Logic and Exponential Backoff

async function fetchWithRetry(url, options = {}, maxRetries = 3, baseDelay = 1000) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, {
        ...options,
        signal: AbortSignal.timeout(10000), // 10s timeout per attempt
      });

      // Don't retry client errors (4xx) except 429 (rate limit)
      if (!response.ok) {
        if (response.status < 500 && response.status !== 429) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        if (attempt < maxRetries) {
          // Respect Retry-After header or use exponential backoff
          const retryAfter = response.headers.get('Retry-After');
          const waitMs = retryAfter
            ? parseInt(retryAfter) * 1000
            : baseDelay * Math.pow(2, attempt - 1);
          console.warn(`Attempt ${attempt}/${maxRetries}: HTTP ${response.status}, retrying in ${waitMs}ms`);
          await new Promise(resolve => setTimeout(resolve, waitMs));
          continue;
        }
        throw new Error(`HTTP ${response.status} after ${maxRetries} attempts`);
      }

      return await response.json();

    } catch (error) {
      if (error.name === 'TimeoutError') throw new Error('Request timed out');
      if (attempt === maxRetries) throw error;
      const waitMs = baseDelay * Math.pow(2, attempt - 1);
      console.warn(`Attempt ${attempt} failed: ${error.message}. Retrying...`);
      await new Promise(resolve => setTimeout(resolve, waitMs));
    }
  }
}

Timeout Wrapper with AbortController

// Generic Promise timeout (works with any Promise)
function withTimeout(promise, ms, message) {
  const timeout = new Promise((_, reject) =>
    setTimeout(
      () => reject(new Error(message ?? `Operation timed out after ${ms}ms`)),
      ms
    )
  );
  return Promise.race([promise, timeout]);
}

// Better for fetch: AbortController actually cancels the request
async function fetchWithAbort(url, timeoutMs = 5000) {
  const controller = new AbortController();
  const timeoutId  = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(url, { signal: controller.signal });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return await response.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error(`Request to ${url} timed out after ${timeoutMs}ms`);
    }
    throw error;
  } finally {
    clearTimeout(timeoutId);  // Always clean up the timer
  }
}

// Usage
try {
  const data = await fetchWithAbort('https://api.example.com/slow-endpoint', 3000);
  renderData(data);
} catch (error) {
  showError(error.message);
}

Concurrent Dashboard Loading

async function loadDashboard(userId) {
  // Start independent requests immediately (before any await)
  const [userPromise, settingsPromise] = [
    fetchUser(userId),
    fetchSettings(userId),
  ];

  // Wait for user first (needed to get user-dependent IDs)
  const user = await userPromise;

  // Now fetch all user-dependent data in parallel
  const [posts, followers, notifications, settings] = await Promise.all([
    fetchPosts(user.id, { limit: 10 }),
    fetchFollowers(user.id),
    fetchNotifications(user.id, { unreadOnly: true }),
    settingsPromise,  // Already in flight — just awaiting its result
  ]);

  return {
    user,
    posts,
    followers,
    notifications,
    settings,
    unreadCount: notifications.filter(n => !n.read).length,
  };
}

9. Common Mistakes and How to Fix Them

Mistake 1: Forgetting await

// WRONG: data is a Promise object, not the actual data
async function wrong() {
  const data = fetchUser(1);     // Missing await! data = Promise{<pending>}
  console.log(data.name);        // undefined — Promise has no .name
  if (data) { ... }              // Always true — Promise objects are truthy
}

// RIGHT: await the Promise to get the resolved value
async function correct() {
  const data = await fetchUser(1);  // data = { id: 1, name: 'Alice', ... }
  console.log(data.name);           // 'Alice'
}

// SUBTLE: forgetting await in a conditional
async function sneakyBug(userId) {
  const user = fetchUserIfExists(userId);  // Missing await!
  if (user) {                              // Always true! Promise is truthy
    await sendWelcomeEmail(user);          // user is a Promise, not a user object
  }
}

// Use ESLint rule @typescript-eslint/no-floating-promises to catch these

Mistake 2: Swallowing Errors (Empty catch blocks)

// WRONG: errors are silently discarded — impossible to debug
async function silentFailure() {
  try {
    await doImportantWork();
  } catch (e) {
    // Empty catch: error gone forever. Network error? Bug? Nobody knows.
  }
}

// RIGHT: always log at minimum, or re-throw
async function properErrorHandling() {
  try {
    await doImportantWork();
  } catch (error) {
    console.error('doImportantWork failed:', error);
    // Option A: Re-throw so the caller can handle it
    throw error;
    // Option B: Return a fallback (only if failure is genuinely acceptable)
    // return fallbackValue;
  }
}

Mistake 3: Nested Promises (Promise Callback Hell)

// WRONG: nested .then() — Promise callback hell
fetchUser(id).then(user => {
  fetchPosts(user.id).then(posts => {         // Nested!
    fetchComments(posts[0].id).then(comments => { // Even deeper!
      renderPage(user, posts, comments);
    });
  });
});

// RIGHT: flat async/await
async function loadPage(id) {
  const user     = await fetchUser(id);
  const posts    = await fetchPosts(user.id);      // Depends on user
  const comments = await fetchComments(posts[0].id); // Depends on posts
  return renderPage(user, posts, comments);
}

Mistake 4: Wrapping a Promise in new Promise() (Anti-Pattern)

// WRONG: wrapping an already-Promise-based API unnecessarily
function fetchDataWrong(url) {
  return new Promise((resolve, reject) => {
    fetch(url)               // fetch() already returns a Promise!
      .then(res => res.json())
      .then(resolve)         // Redundant
      .catch(reject);        // Error handling is also double-handled
  });
}

// RIGHT: just chain or use async/await directly
function fetchDataRight(url) {
  return fetch(url).then(res => {
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  });
}

async function fetchDataAsync(url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

10. Callbacks vs. Promises vs. async/await: Comparison Table

AspectCallbacksPromisesasync/await
ReadabilityPoor (nesting)Good (chaining)Excellent (sync-like)
Error handlingEasy to miss.catch() chaintry/catch blocks
Stack tracesTruncatedPartialFull and clear
Debugging (breakpoints)DifficultModerateExcellent
Parallel executionManual countersPromise.all / .raceawait Promise.all
Conditional async flowComplex nestingModerateSimple (if/else, loops)
TypeScript supportWeakGoodBest (full type inference)
Browser supportUniversalES6+ (IE with polyfill)ES2017+ (Babel for older)
Learning curveLow initiallyMediumLow (if Promises understood)
Best forLegacy codebasesComposing/chainingMost new async code

Frequently Asked Questions

What are the three states of a JavaScript Promise?

A Promise has three states: pending (initial — not yet settled), fulfilled (operation completed successfully with a value), and rejected (operation failed with a reason/error). Once settled (fulfilled or rejected), a Promise's state is permanent and never changes. You handle fulfilled Promises with .then() and rejected ones with .catch().

What is the difference between async/await and .then()/.catch()?

Both are built on the same Promise mechanism. async/await uses synchronous-style syntax making code easier to read, debug, and reason about — especially for sequential operations. Error handling uses familiar try/catch. .then()/.catch() chains are more explicit about the async nature and useful for dynamic composition. async/await compiles to .then()/.catch() under the hood, so choose based on readability. Most modern codebases prefer async/await.

What is the difference between Promise.all and Promise.allSettled?

Promise.all fails fast — if any one promise rejects, the whole Promise.all rejects immediately with that error. Use it when all results are required. Promise.allSettled waits for every promise to settle regardless of outcome and returns an array of { status, value/reason } objects. It never rejects. Use it for batch operations where you want all results, including failures.

How do I run multiple async operations in parallel?

Use Promise.all with an array of started promises: const [a, b] = await Promise.all([fetchA(), fetchB()]). The key is that fetchA() and fetchB() are invoked (started) before any await — this kicks off both operations simultaneously. Never use sequential awaits like: const a = await fetchA(); const b = await fetchB(); for independent operations — that runs them one at a time.

How do I add a timeout to a Promise?

Use Promise.race with a timeout Promise: function withTimeout(p, ms) { return Promise.race([p, new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), ms))]); }. For fetch specifically, use AbortController: const ctrl = new AbortController(); setTimeout(() => ctrl.abort(), ms); await fetch(url, { signal: ctrl.signal }); — this properly cancels the underlying request rather than just abandoning it.

What happens if I forget await before a Promise?

Without await, you receive the Promise object itself rather than its resolved value. The variable will be a Promise, not your data. Comparisons like if (result) are always true (Promises are objects). The Promise's errors will not be caught by surrounding try/catch blocks. TypeScript and ESLint (with the @typescript-eslint/no-floating-promises rule) can catch many forgotten-await bugs at compile time.

What is promisification?

Promisification converts callback-style APIs (where the last argument is a callback(error, result) function) into Promise-returning functions. Node.js provides util.promisify(fn) for standard Node-style callbacks. You can also promisify manually: return new Promise((resolve, reject) => fn(...args, (err, result) => err ? reject(err) : resolve(result))). Node.js also provides fs/promises, dns/promises, and other built-in Promise APIs.

What is Promise.withResolvers() and when is it useful?

Promise.withResolvers() (ES2024) returns { promise, resolve, reject } — letting you access the resolver functions outside the constructor callback. It replaces the "deferred" pattern where you capture resolve/reject via closure. It is especially useful for converting event emitters to Promises, building cancellation tokens, and any case where you need to resolve/reject a Promise from a different scope or callback.

Conclusion

JavaScript Promises and async/await are indispensable tools for modern web development. Mastering them means understanding Promise states, choosing the right combinator (Promise.all vs Promise.allSettled vs Promise.race), handling errors explicitly at every level, and knowing when to run operations in parallel vs. sequentially. Avoid the common pitfalls — forgetting await, swallowing errors in empty catch blocks, and nesting Promises unnecessarily. With these patterns in hand, your async code will be reliable, performant, and easy to maintain. Use our JSON Formatter and JWT Decoder to debug the API responses you fetch with these async patterns.

𝕏 Twitterin LinkedIn
Was this helpful?

Stay Updated

Get weekly dev tips and new tool announcements.

No spam. Unsubscribe anytime.

Try These Related Tools

{ }JSON FormatterJSJavaScript Minifier

Related Articles

JavaScript Array Methods Cheat Sheet: Every Method with Examples

Complete JavaScript array methods reference with practical examples. Covers map, filter, reduce, find, sort, flat, flatMap, splice, and ES2023 methods like toSorted and toReversed.

TypeScript Generics Complete Guide 2026: From Basics to Advanced Patterns

Master TypeScript generics with this comprehensive guide covering type parameters, constraints, conditional types, mapped types, utility types, and real-world patterns like event emitters and API clients.

REST API Best Practices: The Complete Guide for 2026

Learn REST API design best practices including naming conventions, error handling, authentication, pagination, versioning, and security headers.