JavaScript Promises - Complete Guide

Promises are JavaScript objects that represent the eventual completion or failure of an asynchronous operation. This guide covers everything you need to know about Promises.

What is a Promise? #

A Promise is an object that may produce a single value in the future: either a resolved value or a reason for rejection.

Three States:

  1. Pending - Initial state
  2. Fulfilled - Operation completed successfully
  3. Rejected - Operation failed

Creating Promises #

Basic Promise #

const promise = new Promise((resolve, reject) => {
  // Async operation
  const success = true;

  if (success) {
    resolve("Operation successful");
  } else {
    reject("Operation failed");
  }
});

Simple Examples #

// Immediately resolved
const resolvedPromise = Promise.resolve("Success!");

// Immediately rejected
const rejectedPromise = Promise.reject("Error!");

// With timeout
const delayedPromise = new Promise((resolve) => {
  setTimeout(() => {
    resolve("Done after 1 second");
  }, 1000);
});

Consuming Promises #

then() #

Handle successful resolution:

promise
  .then(result => {
    console.log(result);
    return result;
  })
  .then(result => {
    console.log("Chained:", result);
  });

catch() #

Handle errors:

promise
  .then(result => console.log(result))
  .catch(error => console.error(error));

finally() #

Runs regardless of outcome:

promise
  .then(result => console.log(result))
  .catch(error => console.error(error))
  .finally(() => console.log("Cleanup"));

Chaining Promises #

fetch('https://api.example.com/user')
  .then(response => response.json())
  .then(user => fetch(`https://api.example.com/posts/${user.id}`))
  .then(response => response.json())
  .then(posts => console.log(posts))
  .catch(error => console.error(error));

Return values:

  • If you return a value, next .then() receives it
  • If you return a Promise, next .then() waits for it
  • If you throw an error, next .catch() receives it
Promise.resolve(1)
  .then(value => {
    console.log(value);  // 1
    return value + 1;
  })
  .then(value => {
    console.log(value);  // 2
    return value + 1;
  })
  .then(value => {
    console.log(value);  // 3
  });

Promise.all() #

Wait for all promises to resolve:

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

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

Use case: Multiple parallel requests:

Promise.all([
  fetch('/api/users'),
  fetch('/api/posts'),
  fetch('/api/comments')
])
  .then(responses => Promise.all(responses.map(r => r.json())))
  .then(([users, posts, comments]) => {
    console.log(users, posts, comments);
  })
  .catch(error => console.error("One failed:", error));

Note: If any promise rejects, Promise.all() rejects immediately.

Promise.allSettled() #

Wait for all promises to settle (resolve or reject):

const promises = [
  Promise.resolve(1),
  Promise.reject("Error"),
  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
// Success: 3

Promise.race() #

Returns first promise to settle:

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

Promise.race([slow, fast])
  .then(result => console.log(result));  // "fast"

Use case: Timeout for requests:

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

Promise.race([
  fetch('https://api.example.com/data'),
  timeout(5000)
])
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));

Promise.any() #

Returns first promise to fulfill:

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

Promise.any(promises)
  .then(result => console.log(result))  // "Success"
  .catch(error => console.error(error));

Note: Only rejects if all promises reject.

Error Handling #

Basic Error Handling #

promise
  .then(result => {
    // Handle success
  })
  .catch(error => {
    // Handle error
  });

Multiple Catch Blocks #

fetch('/api/data')
  .then(response => {
    if (!response.ok) {
      throw new Error('Network error');
    }
    return response.json();
  })
  .catch(error => {
    console.error('Fetch error:', error);
    throw error;  // Re-throw if needed
  })
  .then(data => processData(data))
  .catch(error => {
    console.error('Processing error:', error);
  });

Try-Catch in Then #

promise
  .then(result => {
    try {
      return JSON.parse(result);
    } catch (error) {
      throw new Error('Parse error');
    }
  })
  .catch(error => console.error(error));

Practical Examples #

Fetch with Retry #

function fetchWithRetry(url, retries = 3) {
  return fetch(url)
    .then(response => {
      if (!response.ok) throw new Error('Request failed');
      return response.json();
    })
    .catch(error => {
      if (retries > 0) {
        console.log(`Retrying... (${retries} left)`);
        return fetchWithRetry(url, retries - 1);
      }
      throw error;
    });
}

fetchWithRetry('https://api.example.com/data')
  .then(data => console.log(data))
  .catch(error => console.error('All retries failed:', error));

Delay Function #

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

// Usage
console.log('Start');
delay(2000)
  .then(() => console.log('After 2 seconds'))
  .then(() => delay(1000))
  .then(() => console.log('After 3 seconds total'));

Sequential Promises #

function sequential(promises) {
  return promises.reduce((chain, promise) => {
    return chain.then(() => promise());
  }, Promise.resolve());
}

// Usage
const tasks = [
  () => delay(1000).then(() => console.log('Task 1')),
  () => delay(1000).then(() => console.log('Task 2')),
  () => delay(1000).then(() => console.log('Task 3'))
];

sequential(tasks);
// Logs: Task 1 (after 1s), Task 2 (after 2s), Task 3 (after 3s)

Parallel with Limit #

async function parallelLimit(tasks, limit) {
  const results = [];
  const executing = [];

  for (const task of tasks) {
    const promise = Promise.resolve().then(() => task());
    results.push(promise);

    if (limit <= tasks.length) {
      const execute = promise.then(() => {
        executing.splice(executing.indexOf(execute), 1);
      });
      executing.push(execute);

      if (executing.length >= limit) {
        await Promise.race(executing);
      }
    }
  }

  return Promise.all(results);
}

Cache with Promises #

const cache = new Map();

function cachedFetch(url) {
  if (cache.has(url)) {
    return Promise.resolve(cache.get(url));
  }

  return fetch(url)
    .then(response => response.json())
    .then(data => {
      cache.set(url, data);
      return data;
    });
}

Promise Queue #

class PromiseQueue {
  constructor() {
    this.queue = [];
    this.running = false;
  }

  add(promiseFunction) {
    return new Promise((resolve, reject) => {
      this.queue.push({ promiseFunction, resolve, reject });
      this.run();
    });
  }

  async run() {
    if (this.running || this.queue.length === 0) return;

    this.running = true;
    const { promiseFunction, resolve, reject } = this.queue.shift();

    try {
      const result = await promiseFunction();
      resolve(result);
    } catch (error) {
      reject(error);
    } finally {
      this.running = false;
      this.run();
    }
  }
}

// Usage
const queue = new PromiseQueue();
queue.add(() => fetch('/api/data1'));
queue.add(() => fetch('/api/data2'));

Promises vs Callbacks #

Callback Hell #

getData(function(a) {
  getMoreData(a, function(b) {
    getMoreData(b, function(c) {
      getMoreData(c, function(d) {
        console.log(d);
      });
    });
  });
});

Promise Chain #

getData()
  .then(a => getMoreData(a))
  .then(b => getMoreData(b))
  .then(c => getMoreData(c))
  .then(d => console.log(d))
  .catch(error => console.error(error));

Converting Callbacks to Promises #

// Callback-based function
function readFile(filename, callback) {
  // ... read file
  callback(error, data);
}

// Promisified version
function readFilePromise(filename) {
  return new Promise((resolve, reject) => {
    readFile(filename, (error, data) => {
      if (error) reject(error);
      else resolve(data);
    });
  });
}

// Usage
readFilePromise('file.txt')
  .then(data => console.log(data))
  .catch(error => console.error(error));

Util.promisify (Node.js) #

const util = require('util');
const fs = require('fs');

const readFilePromise = util.promisify(fs.readFile);

readFilePromise('file.txt', 'utf8')
  .then(data => console.log(data))
  .catch(error => console.error(error));

Promises with Async/Await #

// Promise chain
function fetchUser() {
  return fetch('/api/user')
    .then(response => response.json())
    .then(user => fetch(`/api/posts/${user.id}`))
    .then(response => response.json())
    .then(posts => console.log(posts))
    .catch(error => console.error(error));
}

// Async/await
async function fetchUser() {
  try {
    const response = await fetch('/api/user');
    const user = await response.json();

    const postsResponse = await fetch(`/api/posts/${user.id}`);
    const posts = await postsResponse.json();

    console.log(posts);
  } catch (error) {
    console.error(error);
  }
}

Best Practices #

  1. Always handle errors - Use .catch() or try/catch
  2. Return promises from .then() for chaining
  3. Don’t nest promises - Use chaining instead
  4. Use Promise.all() for parallel operations
  5. Use async/await for cleaner code
  6. Create helper functions for common patterns
  7. Test error cases - Don’t just test success
  8. Use finally() for cleanup
  9. Avoid promise constructors when possible
  10. Document async behavior in code comments

Common Mistakes #

  1. Forgetting to return in .then()
  2. Not handling rejections (unhandled promise rejection)
  3. Creating unnecessary promise wrappers
  4. Mixing callbacks and promises
  5. Not understanding promise state (resolved ≠ fulfilled)

Promises are fundamental to modern JavaScript. Master them to write clean, maintainable asynchronous code.