#100DaysOfMERN - Day 10

Subscribe to my newsletter and never miss my upcoming articles

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 .thenwaits 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 and package-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

Comments (1)

Anand Baraik's photo

Following your series for my 100 days of MERN. Thank you for write-up.