đ¤ JavaScript Promises
Promises
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:
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!! đ');23// setTimeout() function is a method on the window object4// setTimeout() function sets a timer & executes a callback function when the timer expires5// Function signature: setTimeout(callback, delayInMilliseconds, argument1, argument2, ...);6const delayedUserGreeting = () => setTimeout(callback, 2000);78<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 };45 const multiplyByTwo = (value, callback) => {6 setTimeout(callback(value * 2), 1000);7 };89 const divideByTen = (value, callback) => {10 setTimeout(callback(value / 10), 1000);11 };1213 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 };2425 <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 time2const 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};1314// "Consuming Code"... that must wait for a fulfilled Promise15someValidator('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};89const promiseMultiplyByTwo = (value) => {10 return new Promise((resolve) => {11 setTimeout(() => {12 resolve(value * 2);13 }, 1000);14 });15};1617const promiseDivideByTen = (value) => {18 return new Promise((resolve) => {19 setTimeout(() => {20 resolve(value / 10);21 }, 1000);22 });23};2425const 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};3940<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 };1213 const promisesError = (value) => {14 promiseAddOneRejected(value)15 .then((val1) => {16 alert(val1);17 })18 .catch((error) => {19 alert(`Promise rejection caught: ${error}`);20 });21 };2223 <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.
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 2026Life without a modern JavaScript framework. Vanilla JS fun.
@mixin button($padding: 0.75rem) {padding: $padding;border-radius: 8px;}.primary { @include button; }
đĒ Sass Mixins
22 Nov 2020 âĸ đ 5 min read âĸ Updated 15 Mar 2026Creating flexible styles and reusable patterns with Sass mixins.