Online Compiler logoOnline Compiler

JavaScript Promises: A Complete Guide

Promises are a fundamental concept in JavaScript for handling asynchronous operations. They represent a value that may be available now, or in the future, or never. Understanding promises is crucial for modern JavaScript development.

What are Promises?

A Promise is an object representing the eventual completion or failure of an asynchronous operation. It allows you to write asynchronous code that looks synchronous, making it easier to read and maintain.

Basic Promise Creation

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

// Using the promise
myPromise
  .then(result => console.log(result))
  .catch(error => console.error(error));

This example shows how to create a basic promise that resolves or rejects after a timeout.

Promise States

A promise can be in one of three states:

  • Pending: Initial state, neither fulfilled nor rejected
  • Fulfilled: Operation completed successfully
  • Rejected: Operation failed

Promise States Example

const promise = new Promise((resolve, reject) => {
  console.log('Promise created - State: Pending');
  
  setTimeout(() => {
    const random = Math.random();
    if (random > 0.5) {
      resolve('Success!'); // State: Fulfilled
    } else {
      reject('Error!'); // State: Rejected
    }
  }, 1000);
});

promise
  .then(result => console.log('Fulfilled:', result))
  .catch(error => console.log('Rejected:', error));

This demonstrates how a promise transitions from pending to either fulfilled or rejected.

Promise Methods: then(), catch(), finally()

Using then(), catch(), and finally()

const fetchData = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = { id: 1, name: 'John Doe' };
      resolve(data);
      // reject(new Error('Network error'));
    }, 1000);
  });
};

fetchData()
  .then(data => {
    console.log('Data received:', data);
    return data.name.toUpperCase();
  })
  .then(processedData => {
    console.log('Processed data:', processedData);
  })
  .catch(error => {
    console.error('Error:', error.message);
  })
  .finally(() => {
    console.log('Operation completed (success or failure)');
  });

then() handles success, catch() handles errors, and finally() runs regardless of outcome.

Promise Chaining

Promises can be chained to perform sequential asynchronous operations. Each then() returns a new promise, allowing for clean sequential code.

Promise Chaining Example

const getUser = (id) => {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id, name: 'User ' + id }), 500);
  });
};

const getPosts = (userId) => {
  return new Promise((resolve) => {
    setTimeout(() => resolve(['Post 1', 'Post 2', 'Post 3']), 500);
  });
};

const getComments = (postId) => {
  return new Promise((resolve) => {
    setTimeout(() => resolve(['Comment 1', 'Comment 2']), 500);
  });
};

// Chaining promises
getUser(1)
  .then(user => {
    console.log('User:', user);
    return getPosts(user.id);
  })
  .then(posts => {
    console.log('Posts:', posts);
    return getComments(1);
  })
  .then(comments => {
    console.log('Comments:', comments);
  })
  .catch(error => console.error('Error:', error));

This shows how to chain promises to perform sequential async operations.

Promise.all() - Running Promises in Parallel

Promise.all() takes an array of promises and returns a single promise that resolves when all promises in the array have resolved, or rejects if any promise rejects.

Promise.all() Example

const promise1 = new Promise(resolve => setTimeout(() => resolve('First'), 1000));
const promise2 = new Promise(resolve => setTimeout(() => resolve('Second'), 2000));
const promise3 = new Promise(resolve => setTimeout(() => resolve('Third'), 500));

Promise.all([promise1, promise2, promise3])
  .then(results => {
    console.log('All promises resolved:', results);
    // Output: ['First', 'Second', 'Third']
  })
  .catch(error => {
    console.error('One promise rejected:', error);
  });

// With error handling
const promises = [
  fetch('/api/user'),
  fetch('/api/posts'),
  fetch('/api/comments')
];

Promise.all(promises)
  .then(responses => Promise.all(responses.map(res => res.json())))
  .then(data => {
    console.log('All data loaded:', data);
  })
  .catch(error => {
    console.error('Failed to load data:', error);
  });

Promise.all() waits for all promises to resolve or rejects if any fail.

Promise.race() - First to Complete

Promise.race() returns a promise that resolves or rejects as soon as one of the promises in the array resolves or rejects.

Promise.race() Example

const slowPromise = new Promise(resolve => 
  setTimeout(() => resolve('Slow'), 3000));

const fastPromise = new Promise(resolve => 
  setTimeout(() => resolve('Fast'), 1000));

const failingPromise = new Promise((resolve, reject) => 
  setTimeout(() => reject('Failed'), 500));

Promise.race([slowPromise, fastPromise])
  .then(result => console.log('Winner:', result)) // 'Fast'
  .catch(error => console.error('Error:', error));

// With timeout example
const timeout = (ms) => new Promise((_, reject) => 
  setTimeout(() => reject(new Error('Timeout')), ms));

const fetchWithTimeout = (url, timeoutMs) => {
  return Promise.race([
    fetch(url),
    timeout(timeoutMs)
  ]);
};

fetchWithTimeout('/api/data', 5000)
  .then(response => response.json())
  .then(data => console.log('Data:', data))
  .catch(error => console.error('Request failed:', error.message));

Promise.race() resolves with the first promise that completes.

Promise.allSettled() - Wait for All to Complete

Promise.allSettled() waits for all promises to settle (either resolve or reject) and returns an array of objects describing each promise's outcome.

Promise.allSettled() Example

const promises = [
  Promise.resolve('Success 1'),
  Promise.reject('Error 1'),
  Promise.resolve('Success 2'),
  new Promise(resolve => setTimeout(() => resolve('Success 3'), 1000))
];

Promise.allSettled(promises)
  .then(results => {
    results.forEach((result, index) => {
      if (result.status === 'fulfilled') {
        console.log(`Promise ${index + 1} fulfilled:`, result.value);
      } else {
        console.log(`Promise ${index + 1} rejected:`, result.reason);
      }
    });
  });

// Output:
// Promise 1 fulfilled: Success 1
// Promise 2 rejected: Error 1
// Promise 3 fulfilled: Success 2
// Promise 4 fulfilled: Success 3

Promise.allSettled() provides results for all promises, regardless of success or failure.

Error Handling Best Practices

Proper Error Handling

// ❌ Bad: No error handling
function riskyOperation() {
  return fetch('/api/data');
}

// ✅ Good: Proper error handling
function safeOperation() {
  return fetch('/api/data')
    .then(response => {
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return response.json();
    })
    .catch(error => {
      console.error('Fetch failed:', error);
      // Handle error appropriately
      throw error; // Re-throw if needed
    });
}

// Using async/await with try/catch
async function safeAsyncOperation() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Operation failed:', error);
    throw error;
  }
}

Always handle errors properly in promise chains and async functions.

Converting Callbacks to Promises

Promisifying Callback-based Functions

// Original callback-based function
function readFile(path, callback) {
  // Simulate async file reading
  setTimeout(() => {
    if (Math.random() > 0.1) {
      callback(null, 'File content');
    } else {
      callback(new Error('File not found'));
    }
  }, 1000);
}

// Convert to promise
function readFilePromise(path) {
  return new Promise((resolve, reject) => {
    readFile(path, (error, data) => {
      if (error) {
        reject(error);
      } else {
        resolve(data);
      }
    });
  });
}

// Usage
readFilePromise('/path/to/file.txt')
  .then(content => console.log('File content:', content))
  .catch(error => console.error('Error:', error.message));

// Utility function for promisifying
function promisify(originalFunction) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      originalFunction(...args, (error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      });
    });
  };
}

const readFileAsync = promisify(readFile);
readFileAsync('/path/to/file.txt')
  .then(content => console.log(content));

Convert callback-based functions to promise-based for better async handling.

Common Promise Patterns

Retry Pattern

function retryPromise(fn, maxRetries = 3, delay = 1000) {
  return new Promise((resolve, reject) => {
    function attempt(retriesLeft) {
      fn()
        .then(resolve)
        .catch(error => {
          if (retriesLeft > 0) {
            setTimeout(() => attempt(retriesLeft - 1), delay);
          } else {
            reject(error);
          }
        });
    }
    attempt(maxRetries);
  });
}

// Usage
const unreliableFetch = () => {
  return new Promise((resolve, reject) => {
    if (Math.random() > 0.7) {
      resolve('Success');
    } else {
      reject(new Error('Network error'));
    }
  });
};

retryPromise(unreliableFetch, 3, 500)
  .then(result => console.log('Final result:', result))
  .catch(error => console.error('All retries failed:', error));

Implement retry logic for unreliable async operations.

Timeout Pattern

function withTimeout(promise, timeoutMs) {
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Operation timed out')), timeoutMs);
  });
  
  return Promise.race([promise, timeoutPromise]);
}

// Usage
const slowOperation = new Promise(resolve => 
  setTimeout(() => resolve('Done'), 3000));

withTimeout(slowOperation, 2000)
  .then(result => console.log(result))
  .catch(error => console.error(error.message)); // 'Operation timed out'

Add timeout functionality to promises to prevent hanging operations.

Real-World Example: API Calls

Complete API Example with Promises

class ApiService {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }

  request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    
    return fetch(url, {
      headers: {
        'Content-Type': 'application/json',
        ...options.headers
      },
      ...options
    })
    .then(response => {
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      return response.json();
    });
  }

  getUsers() {
    return this.request('/users');
  }

  getUserById(id) {
    return this.request(`/users/${id}`);
  }

  createUser(userData) {
    return this.request('/users', {
      method: 'POST',
      body: JSON.stringify(userData)
    });
  }

  updateUser(id, userData) {
    return this.request(`/users/${id}`, {
      method: 'PUT',
      body: JSON.stringify(userData)
    });
  }

  deleteUser(id) {
    return this.request(`/users/${id}`, {
      method: 'DELETE'
    });
  }
}

// Usage
const api = new ApiService('https://jsonplaceholder.typicode.com');

// Chain multiple API calls
api.getUsers()
  .then(users => {
    console.log('Users:', users.slice(0, 3));
    return api.getUserById(1);
  })
  .then(user => {
    console.log('User details:', user);
    return api.updateUser(1, { name: 'Updated Name' });
  })
  .then(updatedUser => {
    console.log('Updated user:', updatedUser);
  })
  .catch(error => {
    console.error('API Error:', error.message);
  });

Build a complete API service using promises for all HTTP operations.

Key Takeaways

  • Promises represent future values and help manage asynchronous operations
  • Use .then() for success handling and .catch() for errors
  • Promise.all() runs promises in parallel, Promise.race() returns the first result
  • Always handle errors to prevent uncaught promise rejections
  • Promise chaining enables sequential async operations
  • Consider async/await for cleaner syntax (covered in the next section)

Ready to learn async/await? Check out the async/await guide for modern asynchronous JavaScript.