In the world of JavaScript, asynchronous programming is a fundamental concept that often leads developers to a powerful feature: Promises. You've already encountered scenarios where you have to deal with operations that take some time to complete, like fetching data from a server. This is where Promises come into play, offering a more manageable and efficient approach to handle these asynchronous tasks.
Promises in JavaScript represent the eventual completion (or failure) of an asynchronous operation and its resulting value. By freeing developers from the pitfalls of traditional callback-based approaches, Promises provide a cleaner, more readable, and more flexible way to handle asynchronous code. They allow you to write code that assumes a certain step will be completed in the future, letting you write subsequent steps without nesting them deeply in callbacks.
What Is a Javascript Promise
A promise is a representation of the future return of information either successful or failed. It is a placeholder for the result of an asynchronous operation. A promise can be in one of three states:
- Pending: The initial state of a promise. It means that the promise has not been fulfilled or rejected yet.
- Fulfilled: The state of a promise representing a successful operation. This means that the promise has been resolved and now has a value.
- Rejected: The state of a promise representing a failed operation. This means that the promise has been rejected and has a reason for the failure.
Once a promise is fulfilled or rejected, it is considered settled. After a promise is settled, it is immutable, i.e. it can no longer be changed.
Promises are also called futures and deferreds in other languages. They offer a neat abstraction for writing asynchronous code, and allow you to write code that assumes a certain step will be completed in the future, letting you write subsequent steps without nesting them deeply in callbacks.
How to Create a Promise
While most JavaScript developers only deal with promise based code when working with the Fetch API it's important to understand how to create a promise. This will help you understand how to handle promises when working with the Fetch API. Besides, it's a favorite interview question for many companies!
The basic syntax for creating a promise is as follows:
let promise = new Promise((resolve, reject) => {
// executor
// Some asynchronous operation
});
promise; // Promise {<pending>}
The executor is the function that is passed to the promise constructor. It is the function that will be executed by JavaScript. However, this promise isn't much use, it just stays in the pending state forever.
Resolve and Reject Callbacks
The executor takes two arguments: resolve and reject. These are both functions that are passed to the executor. The executor (your function) is responsible for calling one of these functions with the result as an argument when it is done. If the executor never calls either of these functions, the promise will remain in the pending state forever.
// RESOLVE
let resolvingPromise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done"), 1000);
});
resolvingPromise; // Promise {<pending>}
// After 1 second
resolvingPromise; // Promise {<fulfilled>: "done"}
// REJECT
let rejectedPromise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
rejectedPromise; // Promise {<pending>}
// After 1 second
rejectedPromise; // Promise {<rejected>: Error: Whoops!}
As you can see from the above examples, the promise constructor returns a promise object. This object is in one of three states: pending, fulfilled, or rejected. The state of the promise depends on what the executor does.
This takes higher-order functions to the next level! To the promise constructor, you pass a function that takes two functions as arguments!
If the executor fails, then it's common to call the reject function with an error object as you can see in the example.
Getting the Result of a Promise
In this example:
let resolvingPromise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done"), 1000);
});
resolvingPromise; // Promise {<fulfilled>: "done"}
The successful value of the resolvingPromise in this example is the string "done". But how do you get this value? You can use the then method:
let resolvingPromise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done"), 1000);
});
resolvingPromise.then((result) => console.log(result)); // done (after 1 second)
In this example, the then method is called on the promise. The then method takes a callback function as an argument. This callback function is called when the promise is resolved. The callback function is called with the result of the promise as an argument.
Note that you don't have to wait for the promise to resolve before you call the then method. You can call the then method straight away. If the promise is already resolved, the callback function will be called straight away. If the promise is not resolved, the callback function will be called once the promise is resolved.
You can also use the catch method to handle errors:
let rejectedPromise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
rejectedPromise.catch((error) => console.log(error.message)); // Whoops! (after 1 second)
Chaining Promises
The then method returns a promise. This means that you can chain then methods together:
let resolvingPromise = new Promise((resolve, reject) => {
resolve("done");
});
resolvingPromise
.then((result) => {
console.log(result); // done
return result + result;
})
.then((result) => {
console.log(result); // donedone
return result + result;
})
.then((result) => {
console.log(result); // donedonedone
return result;
});
In this example, the then method is called on the promise. The then method takes a callback function as an argument. This callback function is called when the promise is resolved. The callback function is called with the result of the promise as an argument.
You can chain as many .then() and .catch() together as you like. The result of the promise is passed to the next .then() method in the chain.
It's important to note that the value is always tied up in a promise object. This means that you can't access the value directly. You can only access the value in the callback functions passed to the then methods.
You only need to chain then methods together if you want to do something with the result of the promise that is asynchronous.
A Silly Example of a Promise in Action
Today you will make a promise-based pizza restaurant to get your head around how promises work! To start off, look at this example of one in action:
let bakePizza = new Promise((resolve, reject) => {
setTimeout(() => {
let isBurnt = Math.random() >= 0.5;
if (isBurnt) {
reject("pizza was burned in oven");
} else {
resolve("it is cooked deliciously!");
}
}, 1000);
}) // Note no semicolon here
.then((result) => console.log(`Deliver the pizza, ${result}`)) // no semicolon here either
.catch((result) => console.log(`Call the customer, the ${result}`));
This code:
- Creates a new promise object.
- An
isBurntvariable is created. This is a random number between 0 and 1. If the number is greater than or equal to 0.5, the pizza is burnt. Otherwise, it's cooked deliciously. - If the pizza is burnt, the promise is rejected. Otherwise, it's resolved.
The then and catch methods are then chained on the promise (outside the constructor). These are methods that take a callback function as an argument. The then callback is called if the promise is resolved. The catch callback is called if the promise is rejected.
Promises Can't Be Reset
Promises, once resolved, can't be reset. A new one needs to be created. If you have put this in your console, to reset it you would have to refresh your browser, so wrap it all in a function that can create promises at will.
let bakePizza = () => {
new Promise((resolve, reject) => {
setTimeout(() => {
let isBurnt = Math.random() >= 0.5;
if (isBurnt) {
reject("pizza was burned in oven");
} else {
resolve("it is cooked deliciously!");
}
}, 1000);
})
.then((result) => console.log(`Deliver the pizza, ${result}`))
.catch((result) => console.log(`Call the customer, the ${result}`));
};
Now all you need to do to create another pizza is type in bakePizza() to the console.
This will allow us to try to bake another pizza if the first one is burnt. But what if the second one is burnt too? You'll need to bake another one:
let bakePizza = () => {
new Promise((resolve, reject) => {
setTimeout(() => {
let isBurnt = Math.random() >= 0.5;
if (isBurnt) {
reject("pizza was burned in oven");
} else {
resolve("it is cooked deliciously!");
}
}, 1000);
})
.then((result) => console.log(`Deliver the pizza, ${result}`))
.catch((result) => {
// Here expand the catch function
console.log(`Call the customer, the ${result}`); // Call the customer, the pizza was burnt in oven
bakePizza(); // And bake another pizza! This creates a new promise.
});
};
This will now bake pizzas until a good one comes out! Now you will want to deliver this pizza, right? So, we make another promise function to chain on to the back of it:
let bakePizza = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
let isBurnt = Math.random() >= 0.5;
if (isBurnt) {
reject("pizza was burned in oven");
} else {
resolve("it is cooked deliciously!");
}
}, 1000);
})
.then((result) => {
console.log(`Deliver the pizza, ${result}`);
deliverPizza();
})
.catch((result) => {
console.log(`Call the customer, the ${result}, bake another asap!`);
bakePizza();
});
};
let deliverPizza = () => {
return new Promise(function (resolve, reject) {
setTimeout(() => {
let foundWay = Math.random() >= 0.5;
if (foundWay) {
resolve("pizza delivered!");
} else {
reject("driver got lost ಠ_ಠ");
}
}, 1000);
})
.then((result) => console.log(`${result} Another happy customer! (ᵔᴥᵔ)`))
.catch((result) => {
console.log(`Call the customer, the ${result} offer them a discount.`);
deliverPizza();
});
};
let receiveOrder = () => bakePizza().then((result) => deliverPizza());
This set of functions will have 3 possible results:
-
If the pizza is baked correctly, the pizza will be passed to the
deliverPizzapromise:- If the pizza is delivered successfully,
"Pizza delivered"will be logged - If the delivery fails,
"Call the customer and warn them delivery will be delayed, the driver got lost"
- If the pizza is delivered successfully,
-
If its burned in the oven,
"Call the customer and warn them delivery will be delayed, the pizza was burned in oven"will be logged.
Calling bakePizza() in the catch callback will create a new promise. This could lead to an infinite loop if the pizza is always burnt. This is not a good idea in a real application. So to extend this example, you might want to have a counter that counts how many times the pizza has been burnt. If it's burnt more than 3 times, you might want to call the customer and tell them that you can't deliver the pizza.
Summary: Promises in JavaScript
You've made great strides in understanding the intricate details of JavaScript promises and their applications. You've:
- Discovered the concept of Promises in JavaScript as a way to handle asynchronous tasks, such as server requests, with more ease compared to callbacks.
- Learned that a Promise can have three states: pending, fulfilled, or rejected, and becomes immutable once it is settled.
- Practiced creating promises using the
new Promise()constructor and providing an executor function that specifies how to resolve or reject the promise. - Understood the importance of
resolve()andreject()functions within the executor to control the promise’s outcome and the flow of asynchronous operations. - Got the hang of retrieving results from promises using
.then()for successful operations and.catch()for handling rejections or errors. - Explored the power of promise chaining allowing for a sequence of asynchronous steps that can be neatly organized without nesting.
- Delved into a practical (albeit silly) analogy with a pizza-making example that demonstrates promises in action, highlighting how they can resolve into successful pizza delivery or repeat attempts in case of mishaps.
Remember, you'll probably not need to use the new Promise() constructor very often. Most of the time, you'll be using promises that are returned by other functions, such as the fetch() function.