🤝 JavaScript Promises

Promises

Definition

A promise represents an operation that hasn't completed yet. Promises allow you to defer asynchronous operations and handle them at some future point in time.

This concept is particularly important in a single threaded execution model like JavaScript. In browsers, JavaScript runs and blocks on the same single thread where the browser paints and handles user actions. Promises help reduce this blocking behaviour. By deferring asynchronous operations, the main thread isn't blocked from painting or responding to interactions, which means better performance and a faster time to interact.

Callbacks

Most of you reading this will already know what a callback is, or at least used one before! But for others that have stumbled across this article and are wondering what a callback is:

Definition

A callback is a function passed as an argument to another function, This technique allows a function to call another function. A callback function can run after another function has finished.

Wow... we really used the word "function" a lot there. đŸ¤¯ Here's a simple example.

1const callback = () => alert('Hey there!! 👋');
2
3// setTimeout() function is a method on the window object
4// setTimeout() function sets a timer & executes a callback function when the timer expires
5// Function signature: setTimeout(callback, delayInMilliseconds, argument1, argument2, ...);
6const delayedUserGreeting = () => setTimeout(callback, 2000);
7
8<button onClick={delayedUserGreeting}>Click me đŸ›Žī¸</button>

Callback Hell

Callbacks are so common in JavaScript with support for first class functions, but using them can very quickly get out of control. As execution finishes in a function called "step1", we need to call another function called "step2", that calls another called "step3", and so on. It's easy to fall into this function chaining anti-pattern and it can quickly spiral out of control to create "callback hell" or a "pyramid of doom".

Here's an example of callback hell in action.

1const addOne = (value, callback) => {
2 setTimeout(callback(value + 1), 1000);
3 };
4
5 const multiplyByTwo = (value, callback) => {
6 setTimeout(callback(value * 2), 1000);
7 };
8
9 const divideByTen = (value, callback) => {
10 setTimeout(callback(value / 10), 1000);
11 };
12
13 const callbackHell = (val) => {
14 addOne(val, function (newVal1) {
15 alert(newVal1);
16 multiplyByTwo(newVal1, function (newVal2) {
17 alert(newVal2);
18 divideByTen(newVal2, function (newVal3) {
19 alert(newVal3);
20 });
21 });
22 });
23 };
24
25 <button onClick={() => callbackHell(10)}>Click me đŸ›Žī¸</button>

We should try avoid creating a long chain of execution like this with callbacks within callbacks, it can quickly become difficult to reason about and debug, especially for more junior developers! This is where promises can really help us simplify things! 🎉

Using Promises

Promise States

A Promise can be in one of three possible states:

  • pending: initial state, not fulfilled or rejected
  • fulfilled: some operation was successful (we called resolve() function)
  • rejected: some operation failed (we called reject() function)

fulfilled + rejected = settled (handled by .finally())

We can control the state of our promise through the resolve & reject functions returned by the callback we provide to the promise constructor. Here's how to create a promise.

Creating A Promise

We create a new promise using the new keyword before our promise constructor. The constructor for a new promise takes a single callback function as an argument that provides us access to reject and resolve methods.

1// "Producing code"... may be an asynchronous task that takes some length of time
2const someValidator = (value) => {
3 return new Promise((resolve, reject) => {
4 setTimeout(() => {
5 if (typeof value !== 'string') {
6 reject('Error: Value is not a string!');
7 } else {
8 resolve('Success: Value was a string', value);
9 }
10 }, 1000);
11 });
12};
13
14// "Consuming Code"... that must wait for a fulfilled Promise
15someValidator('some string')
16 .then((value) => console.log(value)) /* code if successful */
17 .catch((value) => console.error(value)); /* code if some error */

Promise Object Methods

The promise object has three methods you can call on it:

  • promise.then()
  • promise.catch()
  • promise.finally()

These methods can be chained after one another to handle further actions on a "settled promise". A promise is said to be settled when it is in either a fulfilled or rejected state. Here's the callback hell example from above, rewritten with promises.

1const promiseAddOne = (value) => {
2 return new Promise((resolve) => {
3 setTimeout(() => {
4 resolve(value + 1);
5 }, 1000);
6 });
7};
8
9const promiseMultiplyByTwo = (value) => {
10 return new Promise((resolve) => {
11 setTimeout(() => {
12 resolve(value * 2);
13 }, 1000);
14 });
15};
16
17const promiseDivideByTen = (value) => {
18 return new Promise((resolve) => {
19 setTimeout(() => {
20 resolve(value / 10);
21 }, 1000);
22 });
23};
24
25const promises = (value) => {
26 promiseAddOne(value)
27 .then((val1) => {
28 alert(val1);
29 return promiseMultiplyByTwo(val1);
30 })
31 .then((val2) => {
32 alert(val2);
33 return promiseDivideByTen(val2);
34 })
35 .then((val3) => {
36 alert(val3);
37 })
38};
39
40<button onClick={() => promises(10)}>Click me đŸ›Žī¸</button>

Error Handling

One of the nice things about using promises is the error handling. We can easily catch errors or rejected promises by chaining a .catch() function after our.then() function. Here's an example of rejecting a promise and catching it inside our .catch() function.

1const promiseAddOneRejected = (value) => {
2 return new Promise((resolve, reject) => {
3 setTimeout(() => {
4 if (value >= 100) {
5 reject('🤷 Number is too big!!');
6 } else {
7 resolve(value + 1);
8 }
9 }, 1000);
10 });
11 };
12
13 const promisesError = (value) => {
14 promiseAddOneRejected(value)
15 .then((val1) => {
16 alert(val1);
17 })
18 .catch((error) => {
19 alert(`Promise rejection caught: ${error}`);
20 });
21 };
22
23 <button onClick={() => promisesError(5000)}>Click me đŸ›Žī¸</button>

Final Thoughts

By using promises, we've moved away from the nested callback pattern that can quickly lead to callback hell. In doing this, we've eliminated the indentation level on the root level function that increases with each extra asynchronous task we need to perform. We could go a step further to add some syntactical sugar with async/await but we'll cover this in a future article.

Tip

Since this article was first written, Promise.allSettled() has become widely available. Unlike Promise.all() which rejects as soon as any promise rejects, allSettled waits for every promise to complete and gives you the result of each one, whether it fulfilled or rejected. Handy when you want to fire off multiple independent requests and handle each result separately.

Was this useful?

Thanks for reading! Time for one more?

const el = document.getElementById('app')
renderLoader(el)
const data = await fetchUsers()
el.innerHTML = createHtml(data)

đŸĻ Vanilla JavaScript

13 Dec 2020 â€ĸ 📖 10 min read â€ĸ Updated 15 Mar 2026
🔰 Beginner friendly

Life without a modern JavaScript framework. Vanilla JS fun.

0
0
0
JAVASCRIPT
@mixin button($padding: 0.75rem) {
padding: $padding;
border-radius: 8px;
}
.primary { @include button; }

đŸĒ„ Sass Mixins

22 Nov 2020 â€ĸ 📖 5 min read â€ĸ Updated 15 Mar 2026
⚡ Lightning read

Creating flexible styles and reusable patterns with Sass mixins.

0
0
0
SCSS CSS