Skip to content
FLAVIO COPES
flaviocopes.com
2026

How to use promises in JavaScript

By

Learn how to use JavaScript promises, including pending, fulfilled and rejected states, chaining, error handling, and promise concurrency methods.

~~~

Introduction to promises

A promise represents the eventual completion or failure of an asynchronous operation.

It starts in the pending state. It then becomes fulfilled with a value, or rejected with a reason.

Promises were standardized in ES2015. Async functions provide a cleaner syntax for consuming promises, but they don’t replace them.

Understanding promises is fundamental because many JavaScript APIs return them. The MDN Promise reference lists every current method.

How promises work, in brief

When you create a promise, it starts in the pending state.

The surrounding synchronous code keeps running. Later, the promise becomes fulfilled or rejected. A promise in either state is called settled.

You might also see the word resolved. A resolved promise can still be pending if it follows another promise, so it is not another name for fulfilled.

Which JavaScript APIs use promises?

In addition to your own code and library code, promises are used by standard Web APIs such as the Fetch API and many Service Worker methods.

It’s unlikely that in modern JavaScript you’ll find yourself not using promises, so let’s start diving right into them.


Creating a promise

The Promise API exposes a Promise constructor, which you initialize using new Promise():

let done = true

const isItDoneYet = new Promise((resolve, reject) => {
  if (done) {
    const workDone = 'Here is the thing I built'
    resolve(workDone)
  } else {
    const why = 'Still working on something else'
    reject(new Error(why))
  }
})

The function passed to new Promise() is called the executor. JavaScript calls it immediately with the resolve and reject functions.

Call resolve() when the operation succeeds. Call reject() with an Error when it fails.

The Promise constructor is mainly useful when wrapping an older callback-based API. If an API already returns a promise, use that promise directly.


Consuming a promise

In the last section, we introduced how a promise is created.

Now let’s see how the promise can be consumed or used.

Use .then() to handle a fulfilled value and .catch() to handle a rejection:

isItDoneYet
  .then(value => {
    console.log(value)
  })
  .catch(error => {
    console.error(error)
  })

The executor already ran when isItDoneYet was created. These methods register handlers for its result.


Chaining promises

A promise can be returned to another promise, creating a chain of promises.

A great example of chaining promises is the Fetch API.

Calling fetch() returns a promise. You don’t need to wrap it in new Promise().

Example of chaining promises

const status = response => {
  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`)
  }

  return response
}

const json = response => response.json()

fetch('/todos.json')
  .then(status)
  .then(json)
  .then(data => {
    console.log('Request succeeded with JSON response', data)
  })
  .catch(error => {
    console.log('Request failed', error)
  })

In this example, we call fetch() to get a list of TODO items from the todos.json file found in the domain root, and we create a chain of promises.

When fulfilled, the fetch() promise gives us a Response. Here we use:

response also has a json() method, which returns a promise that will resolve with the content of the body processed and transformed into JSON.

The first handler checks the HTTP status. Throwing an error rejects the promise returned by that .then().

The chain skips the remaining fulfillment handlers and moves to .catch().

If that succeeds instead, it calls the json() function we defined. Since the previous promise, when successful, returned the response object, we get it as an input to the second promise.

In this case, we return the data JSON processed, so the third promise receives the JSON directly:

.then((data) => {
  console.log('Request succeeded with JSON response', data)
})

and we log it to the console.


Handling errors

In the above example, in the previous section, we had a catch that was appended to the chain of promises.

When anything in the chain of promises fails and raises an error or rejects the promise, the control goes to the nearest catch() statement down the chain.

new Promise((resolve, reject) => {
  throw new Error('Error')
}).catch(err => {
  console.error(err)
})

// or

new Promise((resolve, reject) => {
  reject(new Error('Error'))
}).catch(err => {
  console.error(err)
})

Cascading errors

If you throw an error inside .catch(), the promise returned by .catch() is rejected. You can append another .catch() to handle it:

new Promise((resolve, reject) => {
  throw new Error('Error')
})
  .catch(err => {
    throw new Error('Error')
  })
  .catch(err => {
    console.error(err)
  })

Orchestrating promises

Promise.all()

Promise.all() returns a promise that fulfills when every input fulfills. The result keeps the same order as the input.

It rejects as soon as one input rejects.

Example:

const f1 = fetch('/something.json')
const f2 = fetch('/something2.json')

Promise.all([f1, f2])
  .then(res => {
    console.log('Array of results', res)
  })
  .catch(err => {
    console.error(err)
  })

The ES2015 destructuring assignment syntax allows you to also do

Promise.all([f1, f2]).then(([res1, res2]) => {
  console.log('Results', res1, res2)
})

You are not limited to using fetch of course, any promise is good to go.

Promise.allSettled()

Use Promise.allSettled() when you need every outcome, even if some operations fail:

Promise.allSettled([f1, f2]).then(results => {
  console.log(results)
})

Each result has a status of fulfilled or rejected, plus its value or rejection reason.

Promise.any()

Promise.any() fulfills with the first input that fulfills:

Promise.any([f1, f2]).then(result => {
  console.log(result)
})

It ignores rejections unless every input rejects. In that case, it rejects with an AggregateError.

Promise.race()

Promise.race() settles as soon as the first input settles.

This means the result can be a fulfillment or a rejection. See the MDN promise concurrency guide for a precise comparison.

Example:

const promiseOne = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'one')
})
const promiseTwo = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'two')
})

Promise.race([promiseOne, promiseTwo]).then(result => {
  console.log(result) // 'two'
})

To see the difference between Promise.all(), Promise.race() and the other combinators on a visual timeline, try the promise combinators tool.

Common errors

Calling Promise incorrectly

The Promise constructor requires new and an executor function:

const promise = new Promise(resolve => {
  resolve('done')
})

Calling Promise() without new, or calling new Promise() without an executor, throws a TypeError.

~~~

Related posts about js: