DEV Community

Alex Chen
Alex Chen

Posted on

JavaScript Promises: The Deep Dive You Need

JavaScript Promises: The Deep Dive You Need

Promises are everywhere in JS. Understand them deeply.

What Is a Promise?

// A promise is a placeholder for a future value
const promise = fetch('/api/data'); // Returns a promise immediately

// It has 3 states:
// 1. Pending:   Initial state, waiting to resolve or reject
// 2. Fulfilled: Completed successfully (has a value)
// 3. Rejected:  Failed (has a reason/error)

// Once settled → immutable (can't change again)
Enter fullscreen mode Exit fullscreen mode

Creating Promises

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

await delay(1000); // Waits 1 second

// With error handling
function randomSuccess() {
  return new Promise((resolve, reject) => {
    if (Math.random() > 0.5) {
      resolve('Success!');
    } else {
      reject(new Error('Random failure'));
    }
  });
}

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

Chaining

fetch('/api/user')
  .then(res => res.json())           // Transform response
  .then(user => user.posts)          // Extract posts
  .then(posts => posts.filter(p => p.published)) // Filter
  .then(posts => render(posts))     // Use result
  .catch(err => showError(err));     // Handle ANY error in the chain

// Each .then() returns a NEW promise
// Errors propagate down to the nearest .catch()
Enter fullscreen mode Exit fullscreen mode

Error Handling Gotchas

// ❌ Error in .then() is caught by .catch()
Promise.resolve(1)
  .then(() => { throw new Error('Oops') })
  .then(() => console.log("Won't run"))
  .catch(err => console.error("Caught:", err.message)); // "Caught: Oops"

// ❌ .catch() returns a resolved promise (error is "handled")
Promise.reject(new Error('Fail'))
  .catch(err => {
    console.log('Error handled:', err.message);
    return 'fallback'; // This becomes the value for next .then()
  })
  .then(val => console.log(val)); // "fallback" — continues!

// ✅ Re-throw to keep rejecting
Promise.reject(new Error('Fail'))
  .catch(err => {
    console.log('Logging:', err.message);
    throw err; // Re-throw — keeps rejection going
  })
  .then(val => console.log("Won't run"))
  .catch(err => console.error("Final catch:", err.message));

// ⚠️ Unhandled rejection = crash!
const p = Promise.reject(new Error('No catch!'));
// In Node.js: UnhandledRejection warning
// In browser: Console warning + unhandledrejection event
Enter fullscreen mode Exit fullscreen mode

Promise Combinators

const p1 = fetch('/api/users');
const p2 = fetch('/api/posts');
const p3 = fetch('/api/comments');

// All must succeed
const [users, posts, comments] = await Promise.all([p1, p2, p3]);
// If ANY fails → throws immediately (others continue but results lost)

// Wait for all (success or fail)
const results = await Promise.allSettled([p1, p2, p3]);
results.forEach(r => {
  if (r.status === 'fulfilled') useData(r.value);
  else logError(r.reason);
});

// First to resolve wins (ignores rejections)
const first = await Promise.race([
  fetchFastServer(),
  timeout(3000), // Fallback if too slow
]);

// First SUCCESS wins (ignores failures)
const data = await Promise.any([
  fetchPrimary(),
  fetchBackup1(),
  fetchBackup2(),
]);
Enter fullscreen mode Exit fullscreen mode

Advanced Patterns

Promise Caching (Memoization)

class PromiseCache {
  #cache = new Map();

  get(key, factory) {
    if (this.#cache.has(key)) {
      return this.#cache.get(key); // Return existing promise
    }

    const promise = factory().finally(() => {
      // Optional: auto-expire after time
      setTimeout(() => this.#cache.delete(key), 60_000);
    });

    this.#cache.set(key, promise);
    return promise;
  }

  clear() { this.#cache.clear(); }
}

const userCache = new PromiseCache();

// Multiple calls before resolved = only ONE actual request
const user1 = await userCache.get(123, () => fetchUser(123));
const user2 = await userCache.get(123, () => fetchUser(123)); // Same promise!
Enter fullscreen mode Exit fullscreen mode

Request Coalescing

class RequestCoalescer {
  #pending = new Map();

  async request(key, fn) {
    if (this.#pending.has(key)) {
      return this.#pending.get(key); // Join existing request
    }

    const promise = fn().finally(() => this.#pending.delete(key));
    this.#pending.set(key, promise);
    return promise;
  }
}

const coalescer = new RequestCoalescer();

// 100 components call this simultaneously:
// Only 1 API call made, all get the same result
const config = await coalescer.request('app-config', () => fetchConfig());
Enter fullscreen mode Exit fullscreen mode

Retry as a Promise

```function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) {
return new Promise((resolve, reject) => {
let attempt = 0;

const tryAgain = () => {
  fn()
    .then(resolve)
    .catch(error => {
      attempt++;
      if (attempt >= maxRetries) return reject(error);

      const delay = baseDelay * Math.pow(2, attempt - 1);
      setTimeout(tryAgain, delay);
    });
};

tryAgain();
Enter fullscreen mode Exit fullscreen mode

});
}

// Usage
const data = await retryWithBackoff(() => fetch('/api/data'), 5);```

Timeout Wrapper

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

// Usage
const data = await withTimeout(fetch('/api/data'), 5000);
Enter fullscreen mode Exit fullscreen mode

Debugging Promises

// Track unresolved promises
const pendingPromises = new Set();

function track(promise, label) {
  pendingPromises.add({ promise, label, created: Date.now() });
  promise.finally(() => {
    pendingPromises.delete(...pendingPromises.find(p => p.promise === promise));
  });
  return promise;
}

// Check for leaks periodically
setInterval(() => {
  const stale = [...pendingPromises].filter(
    p => Date.now() - p.created > 30_000
  );
  if (stale.length > 0) {
    console.warn('Stale promises:', stale.map(p => p.label));
  }
}, 10_000);
Enter fullscreen mode Exit fullscreen mode

What's your favorite Promise pattern?

Follow @armorbreak for more JavaScript content.

Top comments (0)