Introduction
JavaScript is single-threaded, meaning it executes one operation at a time. But in the real world, applications constantly need to wait for API responses, file loading, or complex computations.
Historically, this reliance on waiting led to complex, nested callback functions, often referred to as “Callback Hell.” Promises were introduced to solve this exact problem, making asynchronous code cleaner, more readable, and far more efficient.
In this guide, we’ll cover the fundamentals of Promises, how to create and consume them, and some common pitfalls to avoid when using Promises
What is a Promise?
A Promise in JavaScript is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. It acts as a placeholder for a value that is not yet available.
Every Promise exists in one of three mutually exclusive states:
- Pending: The initial state; the operation is still ongoing and has neither completed nor failed.
- Fulfilled (or Resolved): The operation completed successfully, and the Promise now holds a resulting value.
- Rejected: The operation failed, and the Promise holds a reason for the failure (usually an Error object).
Think of a Promise like ordering food:
You place the order (Promise is created — Pending).
The food arrives successfully (Promise is Fulfilled —
resolve
).The kitchen can run out of ingredients (Promise is Rejected —
reject
).
How to create a Promise
You create a Promise using the Promise
constructor, which takes one argument: an executor function.
The executor function is responsible for starting the asynchronous work. It receives two arguments, both of which are callback functions and is immediately executed when the promise constructor is called.
-
resolve
: Call this function when the asynchronous operation succeeds. -
reject
: Call this function when the asynchronous operation fails.
Here is a simple example of using Promises
function myExecutor(resolve, reject) {
// async operation and logic to call resolve or reject goes here
const success = Math.random() > 0.5;
setTimeout(function() {
if (success) {
// Operation succeeded, pass the result
resolve("Operation completed successfully!");
} else {
// Operation failed, pass the reason/error
reject(new Error("Operation failed"));
}
}, 2000);
}
const myPromise = new Promise(myExecutor);
console.log(myPromise);
In the above example, setTimeout
is the asynchronous operation that calls (after 2 seconds) the resolve
callback function when the success variable is true
otherwise it calls the reject
callback function when success is false
The myExecutor
named callback function is passed to the Promise
constructor, but we could just as well used an inline anonymous or arrow callback function. Feel free to brush up on callback functions in my previous post on Understanding Callback Functions in JavaScript.
Side Note: Going forward, I’ll be declaring callback functions using anonymous functions. I thing it’s easier to tell that an argument is a function if it literally has the function keyword in it. You can follow along using your preferred method.
Return Value of a Promise
A Promise does not return a value like a traditional function does, instead it provides a state which represents the eventual completion (success or failure)
This is what will get logged to the console from the last line by running the code snippet above;
Promise {<pending>}
It’s a Promise object with the pending
state because that’s the initial state of every Promise, clicking on it in a browser console expands the object to show other possible states and return values.
Consuming Promises
Once a Promise is created, you use methods like .then()
and .catch()
to "consume" its eventual value. These methods allow you to define what happens when the Promise transitions from Pending to either Fulfilled or Rejected.
The .then()
Method
The .then()
method takes up to two handler functions as arguments:
- The first function (
onFulfilled
) accepts as an argument the result ( value returned from the*resolve*
callback function ) and runs when the Promise is Fulfilled. - The second function (
onRejected
) runs when the Promise is Rejected but it is optional and often skipped in favor of.catch()
.
Using the simple example from before, we can consume the value returned by the resolve or rejected callback function by using the .then
method and the handler functions ( onFulfilled
/ onRejected
)
myPromise.then(
function (result) {
// This is the onFulfilled handler that runs if resolve() was called.
console.log(result); // logs "Operation completed successfully!"
},
function (error) {
// This is the onRejected handler runs if reject() was called.
console.error(error.message); // Logs "Operation failed"
}
)
Notice we log
error.message
to the console not justerror
because we passed anError
object to thereject()
callback function not a string.
The .catch()
Method
The .catch()
method is syntactic sugar for .then(null, onRejected)
. It is the standard way to handle errors in Promises and is cleaner than putting the rejection handler inside .then()
. We simply just moved the onRejected
handler function to a different method.
The code snippet above can be rewritten as;
myPromise
.then(function (result) {
// This is the onFulfilled handler that runs if resolve() was called
console.log(result); // logs "Operation completed successfully!"
})
.catch(function (error) {
// This runs if reject() was called.
console.error(error.message); // Logs "Operation failed"
})
How Promises Work
The image below can help visualize how Promises work
Source: Shuai Li
From the diagram above, we can see that the state changes from pending to fulfilled if the async operation resolve
with a value, that value is further passed to the .then()
method for further operations.
The state can also change from pending to rejected if the async operation reject
with an Error, that Error is further passed to the .catch()
method for further handling
Chaining Promises
The greatest power of Promises is their ability to be chained. Because the .then()
method always returns a new Promise, you can link multiple asynchronous operations together sequentially.
This is the key mechanism for escaping “Callback Hell,” as you can transform complex, deeply nested code into a flat, readable sequence.
A common example of this is the chaining done when fetching data using the fetch API:
fetch('https://example.com/api/users') // fetch() returns Promise 1
.then(function (response) {
// Process response from Promise 1 and return Promise 2
return response.json(); // response.json() returns a Promise
})
.then(function (data) {
// Use the result of Promise 2
console.log(data);
})
.catch(function (error) {
// Catches any error from all steps in the chain
console.error('Something went wrong:', error);
})
Common Pitfalls to Avoid When Using Promises
Promises are a powerful tool for handling asynchronous operations in JavaScript, but they can be tricky to use correctly. Here are some common pitfalls to watch out for when working with Promises and how to avoid them.
(1) Forgetting That Promises Are Asynchronous:
Although promises can make our code look and feel synchronous, they are still asynchronous in nature, one can easily make the mistake of trying to access the resolved value synchronously.
const getData = () => {
fetch('https://example.com/api/users')
.then(function (response) {
return response.json();
})
.then(function (data) {
return data; // Expecting to use the data outside the getData function
})
.catch(function (error) {
return error;
})
}
const result = getData(); // getData returns a Promise
console.log(result); // Logs Promise { <pending> }, not the data
Because getData()
function contains a Promise, it runs asynchronously so when the JavaScript engine reaches the line const result = getData()
it simply assigns the initial pending state Promise Object to result variable before proceeding to the next line and not the data we expected.
To fix this, we can;
- chain
getData()
with another.then()
method without assigning it to a variable
const getData = () => {
fetch('https://example.com/api/users')
.then(function (response) {
return response.json()
})
.then(function (data) {
return data
})
.catch(function (error) {
return error;
})
}
getData().then(function (result) {
console.log(result); // Logs the data
})
- or use the top level
await
keyword before callinggetData()
like this:
const getData = () => {
fetch('https://example.com/api/users')
.then(function (response) {
return response.json()
})
.then(function (data) {
return data
})
.catch(function (error) {
return error;
})
}
const result = await getData(); // JS awaits the data before proceeding to the next line
console.log(result); // Logs the data
(2) Not Returning Promises in .then()
Chains:
When chaining .then()
calls, forgetting to return a Promise inside a .then()
handler breaks the chain and can cause unexpected behavior.
fetch('https://example.com/api/users')
.then(function (response) {
response.json(); // forgetting the return keyword
})
.then(function (data) {
console.log(data); // data is undefined because the previous chain did not return a promise
})
.catch(function (error) {
console.error('Something went wrong:', error);
})
To fix this; always return a Promise or value from .then()
handlers.
(3) Failing to handle Promise Rejections/Failures:
If a Promise rejects and you don’t handle the error, it can cause unhandled Promise rejection warnings or crashes.
fetch('https://exmpl.com/api/users') // Typo that'll trigger reject()
.then(function (response) {
return response.json();
})
.then(function (data) {
console.log(data);
})
// No .catch() to handle errors
Conclusion
Promises are one of the most important features in modern JavaScript because they simplify how we deal with asynchronous code. Instead of juggling nested callbacks and messy logic, Promises give us a clean, structured way to handle success and failure. They’re the foundation for even more powerful tools like async/await
, which build on the same concepts but make code look synchronous.
As you work with Promises, remember the key ideas: a Promise represents a value that may be available now, later, or never. It can be pending, fulfilled, or rejected—and how you handle those outcomes determines how reliable your code will be.
By mastering Promises, you’ll not only write more maintainable and bug-free code, but you’ll also set yourself up to understand advanced JavaScript concepts with ease. The next time you fetch data, process files, or chain multiple async operations, you’ll see just how powerful Promises really are.
Top comments (4)
Thanks for posting this! More content about promises is always a good thing! There are a couple of things I think could cause confusion, though.
Saying "breaks the chain" when talking about failing to return a promise could be misleading. The chain remains intact, we're just passing
undefined
as the value of the promise, just as any function without a return statement returnsundefined
. It can cause unexpected behavior, but in the same way failing to return a value from any function does.And while I understand the use of
function
keyword for clarity, the use of arrow function "lambdas" can be really useful in preserving the readability of the promise chain for simple operations. We can even forego the anonymous functions altogether in some places. Taking your example, we can eliminate a lot of "boilerplate" while processingresponse.json()
, and just passconsole.log
directly.Also, we don't have to return a promise. That's one of the things I really like about promise chains; the chain maintains the promise structure and will wrap other returns for us. This allows us to call utilities and other operations in the chain without worrying about requiring or returning promises.
While I wouldn't recommend chaining every synchronous function this way, it is useful, especially when testing or mocking functions, as once a promise chain is started we can use synchronous or asynchronous functions inside.
Side note: I would recommend setting the Canonical URL of the article to your Medium post so there's a link between the two instance of the article for SEO optimization.
Thank you so much for your detailed review!
I tried to make the post as beginner friendly as possible hence the verboseness of the code snippets. I'll implement your suggestions in my subsequent posts.
Thank you for a SPLENDID description of promises and how to use them. In particular, the addition of the getData outermost arrow-function explanation is going to save many newbies a LOT of time (and unpulled hair). Your use of functions here, instead of arrow-function-(lambda) shortcuts is a most-welcome newbie-friendly style.
You're welcome.
Thank you also for reading and finding it helpful.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.