Yesterday, I looked into how to create a very basic network request with XMLHttpRequest
and a callback function. The approach is rather verbose, but therefore also very descriptive. The downsides of it aren't obvious with such a simple request, but if you'd start extending the functionality, you'd find yourself writing more and more callback functions, each waiting for the previous to return, before it can continue. The resulting situation has many names, but whether you call it the pyramid of doom, the triangle of death, or simply callback hell - modern JavaScript gives us the possibility to avoid this altogether and write asynchronous code in a more synchronous way. The basis of all this are Promises.
✏ Promises
A Promise is a JavaScript object that serves as a placeholder. If you try to retrieve some data through a network request, you can't know beforehand when the data arrives. You can't even know if the data ever arrives. A Promise is a way of dealing with this situation. It can only have one of these three states:
The initial state, right after it was created, is Pending. From here, it can either transition to Fulfilled (data successfully retrieved) or Rejected (operation failed).
✏ Creating a Promise
Most of the time, you won't create Promises, but rather consume them. For instance, if you try to get data from a server using the more modern fetch()
method instead of an XMLHttpRequest
, that method will return a Promise. However, it makes sense to see how they're created, in order to know how to handle them.
A Promise is an object that takes a callback function as argument. That function contains the logic for the two possible outcomes (Fullfilled or Rejected) and provides itself two callback functions to handle both cases (commonly called resolve and reject, but you can name them however you like).
I'll start with creating a simple Promise:
const callback = (resolve, reject) => {};
const myPromise = new Promise(callback);
Inspecting this in the console, you'll see that the status is Pending. I haven't provided any logic in the callback, so that's hardly going to change, it'll just hang there forever. To help that, I'll modify the callback accordingly - whatever I pass into the resolve method as parameter will be the value that the Promise resolves to, after it reached status Fulfilled.
Usually, you'd add some conditions to decide whether the resolve or the reject method should be called, but I'll start simple and let it auto-resolve.
const callback = (resolve, reject) => {
resolve('RESOLVED');
}
I can also auto-reject it:
const callback = (resolve, reject) => {
reject('REJECTED');
}
Note that once a Promise has reached either Fulfilled or Rejected, it is considered "settled". That means that it has reached its final state and its status won't ever change again after that.
This doesn't mean that calling resolve will exit the callback function. You can still run code after that:
const callback = (resolve, reject) => {
resolve('RESOLVED');
console.log('promise was resolved'); // this will log
reject('CHANGED MY MIND') // this will be ignored
}
✏ Consuming a Promise
Right now, I still can't get the data that's returned by my resolved promise (which is just the string 'RESOLVED' in the above example).
Promises have a method .then
that can be chained. It returns a new Promise, and like all Promises, it takes a callback to pass in the handler functions for both possible outcomes. As the name of the method suggests, the callback won't run until myPromise
is settled. It'll wait for the outcome. If myPromise
remains in Pending state, the callback of then
will never run.
To avoid confusion, I'll name the .then
method's callbacks success
and failure
. To illustrate what's happening, I'll declare a variable myData
. If myPromise
resolves to Fulfilled (which will happen because it's auto-resolving), I want to assign whatever the .resolve
method returns to myData
. It's undefined
to begin with, but after 1 second, I have my data available:
let myData;
const callback = (resolve, reject) => {
resolve('RESOLVED');
}
const success = val => myData = val;
const failure = err => console.log(err);
const myPromise = new Promise(callback).then(success, failure);
console.log(`myData is: ${myData}`) // myData is: undefined
setTimeout(() => {
console.log(`myData is: ${myData}`) // myData is: RESOLVED
}, 1000)
The beauty of this syntax is that .then
waits for the outcome, that's why I can write asynchronous code as if it was synchronous, just by chaining.then
after .then
. There'll be no nesting of callbacks, and the code is easy to reason with.
This was just a very brief introduction to Promises in JavaScript. To really see the benefits in action, the next step is to rewrite the network request from yesterday, so it uses Promises instead of callbacks.
✏ Recap
I've learned
- the basics of Promises in JavaScript
✏ Next:
- rewriting the
XMLHttpRequest
from yesterday
✏ Thanks for reading!
I do my best to thoroughly research the things I learn, but if you find any errors or have additions, please leave a comment below, or @ me on Twitter. If you liked this post, I invite you to subsribe to my newsletter. Until next time 👋
✏ Previous Posts
- Day 1: Introduction, Node.js, Node.js in the terminal
- Day 2:
npm
, node_modules,package.json
andpackage-lock.json
, local vs global installation of packages - Day 3: Create a React app without create-react-app, Webpack, Babel
- Day 4:
npx
and cowsay - Day 5:
npm
vs.npx
,npm audit
, semantic versioning and update rules - Day 6: Call stack, event loop, JavaScript engine, JavaScript runtime
- Day 7: Call stack and event loop in Node.js,
setImmediate()
- Day 8:
setImmediate()
,process.nextTick()
, event loop phases in Node.js - Day 9: Network requests with
XMLHttpRequest
and callbacks