Async JavaScriptFeatured

JavaScript Promises: The Ultimate Guide

Master JavaScript Promises from basics to advanced patterns. Learn Promise creation, chaining, error handling, and real-world applications.

By JavaScriptDoc Team
promisesasynchronousjavascriptcallbacksthencatch

JavaScript Promises: The Ultimate Guide

Promises are a fundamental feature in modern JavaScript for handling asynchronous operations. They provide a cleaner alternative to callbacks and form the foundation for async/await syntax.

What is a Promise?

A Promise is an object representing the eventual completion or failure of an asynchronous operation. Think of it as a placeholder for a value that will be available in the future.

// Real-world analogy
// Promise = "I promise to return your book"
// Pending = "I still have your book"
// Fulfilled = "Here's your book back!"
// Rejected = "Sorry, I lost your book"

const promise = new Promise((resolve, reject) => {
  // Asynchronous operation
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('Operation completed!');
    } else {
      reject('Operation failed!');
    }
  }, 1000);
});

Creating Promises

Basic Promise Constructor

const myPromise = new Promise((resolve, reject) => {
  // The executor function runs immediately
  console.log('Promise executor running');

  // Simulate async operation
  setTimeout(() => {
    const randomNumber = Math.random();

    if (randomNumber > 0.5) {
      resolve(randomNumber); // Success
    } else {
      reject(new Error('Number too small')); // Failure
    }
  }, 1000);
});

// Using the promise
myPromise
  .then((value) => console.log('Success:', value))
  .catch((error) => console.error('Error:', error));

Promise States

A Promise can be in one of three states:

// 1. Pending - initial state
const pendingPromise = new Promise((resolve) => {
  // Never resolves or rejects
});
console.log(pendingPromise); // Promise {<pending>}

// 2. Fulfilled - operation completed successfully
const fulfilledPromise = Promise.resolve('Success!');
console.log(fulfilledPromise); // Promise {<fulfilled>: "Success!"}

// 3. Rejected - operation failed
const rejectedPromise = Promise.reject('Failure!');
console.log(rejectedPromise); // Promise {<rejected>: "Failure!"}

Promise Methods

then() and catch()

// Basic usage
promise
  .then((result) => {
    console.log('Success:', result);
    return result * 2; // Return value passed to next then
  })
  .then((doubled) => {
    console.log('Doubled:', doubled);
  })
  .catch((error) => {
    console.error('Error:', error);
  })
  .finally(() => {
    console.log('Cleanup operations');
  });

// Multiple handlers
const promise = Promise.resolve(42);

// Each gets the same value
promise.then((x) => console.log('First:', x));
promise.then((x) => console.log('Second:', x));
promise.then((x) => console.log('Third:', x));

Promise Chaining

// Sequential operations
function fetchUser(id) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id, name: 'John Doe' });
    }, 1000);
  });
}

function fetchPosts(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 1, userId, title: 'First Post' },
        { id: 2, userId, title: 'Second Post' },
      ]);
    }, 1000);
  });
}

function fetchComments(postId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 1, postId, text: 'Great post!' },
        { id: 2, postId, text: 'Thanks for sharing!' },
      ]);
    }, 1000);
  });
}

// Chain promises
fetchUser(1)
  .then((user) => {
    console.log('User:', user);
    return fetchPosts(user.id);
  })
  .then((posts) => {
    console.log('Posts:', posts);
    return fetchComments(posts[0].id);
  })
  .then((comments) => {
    console.log('Comments:', comments);
  })
  .catch((error) => {
    console.error('Error in chain:', error);
  });

Error Handling

Catching Errors

// Errors propagate through the chain
Promise.resolve('Start')
  .then((value) => {
    console.log(value);
    throw new Error('Something went wrong!');
  })
  .then((value) => {
    // This is skipped
    console.log("This won't run");
  })
  .catch((error) => {
    console.error('Caught:', error.message);
    return 'Recovered'; // Can recover from error
  })
  .then((value) => {
    console.log('Continuing with:', value); // "Recovered"
  });

// Multiple catch blocks
promise
  .then(handleSuccess)
  .catch(handleError)
  .then(processResult)
  .catch(handleProcessingError);

Error Types

// Different error scenarios
function riskyOperation() {
  return new Promise((resolve, reject) => {
    const random = Math.random();

    if (random < 0.33) {
      reject(new Error('General error'));
    } else if (random < 0.66) {
      reject(new TypeError('Type error'));
    } else {
      resolve('Success');
    }
  });
}

riskyOperation()
  .then((result) => console.log(result))
  .catch((error) => {
    if (error instanceof TypeError) {
      console.error('Type error occurred:', error);
    } else if (error instanceof Error) {
      console.error('General error:', error);
    } else {
      console.error('Unknown error:', error);
    }
  });

Static Promise Methods

Promise.resolve() and Promise.reject()

// Creating resolved promises
const resolved1 = Promise.resolve(42);
const resolved2 = Promise.resolve({ name: 'John' });

// Promise.resolve with a promise returns the same promise
const existingPromise = new Promise((r) => r('Hello'));
const wrapped = Promise.resolve(existingPromise);
console.log(wrapped === existingPromise); // true

// Creating rejected promises
const rejected = Promise.reject(new Error('Failed'));

// Useful for starting chains
Promise.resolve()
  .then(() => fetchData())
  .then((data) => processData(data))
  .catch(handleError);

Promise.all()

Waits for all promises to fulfill or any to reject:

const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);

Promise.all([promise1, promise2, promise3]).then((results) => {
  console.log(results); // [1, 2, 3]
});

// Real-world example
async function fetchUserData(userId) {
  const promises = [
    fetchUser(userId),
    fetchPosts(userId),
    fetchFriends(userId),
  ];

  try {
    const [user, posts, friends] = await Promise.all(promises);
    return { user, posts, friends };
  } catch (error) {
    console.error('Failed to fetch user data:', error);
  }
}

// Fails fast - if any promise rejects
Promise.all([
  Promise.resolve(1),
  Promise.reject(new Error('Failed')),
  Promise.resolve(3),
]).catch((error) => {
  console.error(error.message); // "Failed"
});

Promise.allSettled()

Waits for all promises to settle (fulfill or reject):

const promises = [
  Promise.resolve(1),
  Promise.reject(new Error('Failed')),
  Promise.resolve(3),
];

Promise.allSettled(promises).then((results) => {
  results.forEach((result) => {
    if (result.status === 'fulfilled') {
      console.log('Success:', result.value);
    } else {
      console.log('Failed:', result.reason);
    }
  });
});

// Output:
// Success: 1
// Failed: Error: Failed
// Success: 3

// Practical use case
async function batchOperation(items) {
  const operations = items.map((item) =>
    processItem(item).catch((err) => ({ error: err, item }))
  );

  const results = await Promise.allSettled(operations);

  const successful = results
    .filter((r) => r.status === 'fulfilled')
    .map((r) => r.value);

  const failed = results
    .filter((r) => r.status === 'rejected')
    .map((r) => r.reason);

  return { successful, failed };
}

Promise.race()

Resolves/rejects with the first promise to settle:

const slow = new Promise((resolve) => setTimeout(() => resolve('slow'), 2000));
const fast = new Promise((resolve) => setTimeout(() => resolve('fast'), 1000));
const faster = new Promise((resolve) =>
  setTimeout(() => resolve('faster'), 500)
);

Promise.race([slow, fast, faster]).then((winner) => {
  console.log(winner); // "faster"
});

// Timeout implementation
function timeout(ms) {
  return new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), ms)
  );
}

function fetchWithTimeout(url, ms = 5000) {
  return Promise.race([fetch(url), timeout(ms)]);
}

// First to fail also wins
Promise.race([
  new Promise((_, reject) => setTimeout(() => reject('Error'), 1000)),
  new Promise((resolve) => setTimeout(() => resolve('Success'), 2000)),
]).catch((error) => {
  console.log('First to settle was rejection:', error); // "Error"
});

Promise.any()

Resolves with the first fulfilled promise:

const promises = [
  Promise.reject('Error 1'),
  Promise.reject('Error 2'),
  Promise.resolve('Success!'),
  Promise.resolve('Also success'),
];

Promise.any(promises)
  .then((value) => {
    console.log(value); // "Success!" (first to fulfill)
  })
  .catch((error) => {
    console.log('All promises rejected:', error);
  });

// All rejected scenario
Promise.any([
  Promise.reject('Error 1'),
  Promise.reject('Error 2'),
  Promise.reject('Error 3'),
]).catch((error) => {
  console.log(error); // AggregateError: All promises were rejected
  console.log(error.errors); // ["Error 1", "Error 2", "Error 3"]
});

Converting Callbacks to Promises

Promisifying Functions

// Original callback-based function
function readFileCallback(filename, callback) {
  fs.readFile(filename, 'utf8', (err, data) => {
    if (err) callback(err, null);
    else callback(null, data);
  });
}

// Convert to Promise
function readFilePromise(filename) {
  return new Promise((resolve, reject) => {
    fs.readFile(filename, 'utf8', (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

// Generic promisify function
function promisify(fn) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      fn(...args, (err, result) => {
        if (err) reject(err);
        else resolve(result);
      });
    });
  };
}

// Usage
const readFile = promisify(fs.readFile);
readFile('file.txt', 'utf8')
  .then((data) => console.log(data))
  .catch((err) => console.error(err));

Working with Event Emitters

function waitForEvent(emitter, eventName) {
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => {
      reject(new Error(`Timeout waiting for ${eventName}`));
    }, 5000);

    emitter.once(eventName, (data) => {
      clearTimeout(timeout);
      resolve(data);
    });

    emitter.once('error', (error) => {
      clearTimeout(timeout);
      reject(error);
    });
  });
}

// Usage
const server = createServer();
waitForEvent(server, 'listening')
  .then(() => console.log('Server is ready'))
  .catch((err) => console.error('Server failed to start:', err));

Advanced Promise Patterns

Sequential Execution

// Process array items sequentially
async function processSequentially(items, processor) {
  const results = [];

  for (const item of items) {
    const result = await processor(item);
    results.push(result);
  }

  return results;
}

// Using reduce for sequential promises
function processSequentiallyReduce(items, processor) {
  return items.reduce((promise, item) => {
    return promise.then((results) =>
      processor(item).then((result) => [...results, result])
    );
  }, Promise.resolve([]));
}

Batch Processing

async function processBatches(items, batchSize, processor) {
  const results = [];

  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    const batchResults = await Promise.all(
      batch.map((item) => processor(item))
    );
    results.push(...batchResults);
  }

  return results;
}

// With concurrency limit
class PromisePool {
  constructor(concurrency) {
    this.concurrency = concurrency;
    this.current = 0;
    this.queue = [];
  }

  async run(fn) {
    while (this.current >= this.concurrency) {
      await new Promise((resolve) => this.queue.push(resolve));
    }

    this.current++;

    try {
      return await fn();
    } finally {
      this.current--;
      const next = this.queue.shift();
      if (next) next();
    }
  }
}

Memoization

function memoizePromise(fn) {
  const cache = new Map();

  return function (...args) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key);
    }

    const promise = fn
      .apply(this, args)
      .then((result) => {
        cache.set(key, Promise.resolve(result));
        return result;
      })
      .catch((error) => {
        cache.delete(key); // Remove failed promises
        throw error;
      });

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

// Usage
const memoizedFetch = memoizePromise(fetch);

// First call makes the request
memoizedFetch('/api/data').then(handleData);

// Subsequent calls return cached promise
memoizedFetch('/api/data').then(handleData); // No new request

Circuit Breaker Pattern

class CircuitBreaker {
  constructor(fn, threshold = 5, timeout = 60000) {
    this.fn = fn;
    this.threshold = threshold;
    this.timeout = timeout;
    this.failures = 0;
    this.nextTry = Date.now();
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
  }

  async call(...args) {
    if (this.state === 'OPEN') {
      if (Date.now() < this.nextTry) {
        throw new Error('Circuit breaker is OPEN');
      }
      this.state = 'HALF_OPEN';
    }

    try {
      const result = await this.fn(...args);
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failures = 0;
    this.state = 'CLOSED';
  }

  onFailure() {
    this.failures++;
    if (this.failures >= this.threshold) {
      this.state = 'OPEN';
      this.nextTry = Date.now() + this.timeout;
    }
  }
}

Promise Anti-Patterns

The Explicit Construction Anti-Pattern

// Bad - unnecessary promise construction
function bad() {
  return new Promise((resolve, reject) => {
    fetchData()
      .then((data) => resolve(data))
      .catch((err) => reject(err));
  });
}

// Good - return the promise directly
function good() {
  return fetchData();
}

// Bad - wrapping synchronous code
function badSync() {
  return new Promise((resolve) => {
    resolve(42);
  });
}

// Good - use Promise.resolve
function goodSync() {
  return Promise.resolve(42);
}

Forgetting to Return

// Bad - broken chain
promise
  .then((data) => {
    processData(data); // Missing return
  })
  .then((result) => {
    console.log(result); // undefined
  });

// Good
promise
  .then((data) => {
    return processData(data);
  })
  .then((result) => {
    console.log(result);
  });

Nested Promises

// Bad - promise hell
getData().then((data) => {
  return processData(data).then((processed) => {
    return saveData(processed).then((saved) => {
      return notifyUser(saved);
    });
  });
});

// Good - flat chain
getData()
  .then((data) => processData(data))
  .then((processed) => saveData(processed))
  .then((saved) => notifyUser(saved));

// Better - async/await
async function handleData() {
  const data = await getData();
  const processed = await processData(data);
  const saved = await saveData(processed);
  return notifyUser(saved);
}

Testing Promises

// Using async/await in tests
describe('Promise tests', () => {
  test('should resolve with correct value', async () => {
    const result = await Promise.resolve(42);
    expect(result).toBe(42);
  });

  test('should reject with error', async () => {
    await expect(Promise.reject(new Error('Failed'))).rejects.toThrow('Failed');
  });

  test('should handle async operations', async () => {
    const mockFetch = jest.fn().mockResolvedValue({ data: 'test' });
    const result = await mockFetch();
    expect(result.data).toBe('test');
  });
});

// Testing with done callback
test('promise resolves', (done) => {
  Promise.resolve('success').then((value) => {
    expect(value).toBe('success');
    done();
  });
});

Best Practices

  1. Always handle errors - Add .catch() or try-catch
  2. Return promises from .then() - Maintain the chain
  3. Use Promise.all() for parallel operations
  4. Avoid the explicit construction anti-pattern
  5. Consider async/await for complex flows
  6. Add timeouts for external operations
  7. Clean up resources in finally
  8. Use proper error types and messages

Conclusion

Promises are a powerful tool for handling asynchronous operations in JavaScript. Key takeaways:

  • Promises represent future values
  • Three states: pending, fulfilled, rejected
  • Chain operations with .then() and handle errors with .catch()
  • Static methods like Promise.all() enable complex async patterns
  • Avoid common anti-patterns for cleaner code
  • Modern async/await syntax builds on Promises

Master Promises to write more maintainable asynchronous JavaScript code!