The Promises guide I would have loved as a junior developper

ยท

9 min read

Do you remember stumbling on your first "Promise {pending}" ? Putting a .then just to see if it magically work ? This article is made for you, I'll take you with me in my process of re-learning the basics bricks of the JS ecosystem.

The scope of this article is basically from understanding Promises, (how to write and recognize them), to an async/await dive. Fell free to skip to any parts with the table of contents below.

Disclaimer : We won't see the 'hidden' part of Promises. By hidden, I mean how the Javascript engine handle them inside the call stack.

Table of contents

Necessary background informations

The MDN web documentation definition for promises is the following : A Promise is a proxy for a value not necessarily known when the promise is created. It allows you to associate handlers with an asynchronous action's eventual success value or failure reason

If it's unclear to you as it is for me, stay with me, we'll clear everything up, I promise ๐Ÿ˜‰.

So, what are Promises ?

Now that we let this bad joke behind, let's dive straigt into it.

First, the basics. Javascript is a synchronous and mono-threaded language. It means that all your code will execute in the order it's write, and it only has one call stack. To keep it simple, we'll stand that JS is a language where eveything happen in order, without any external add-ons.

Promises are a way to execute certain pieces of code asynchronously, meaning they'll be executed behind the scenes, while the rest of the synchronous code is done.

Why do we need them ?

Let's take a real life exemple with two waiters. To keep it simple, our waiters are responsible from taking the orders, and delivering the dishes froms the kitchen to the clients.

Our first waiter will be the synchronous one (like if Javascript promises never existed). He would take the order, give it to the kitchen, wait for the dishes to be ready, and finally serve them, and so on. Kinda awkward and inefficient.

Our second waiter will handle things asynchronously. He'll take the orders from the clients and give it to the kitchen. By the time the dishes are ready, he will go do something else and come back later for the dishes whenever they are ready.

This is exactly what's happening in JS. The kitchen will give a promise to the waiter that the dishes will be ready sometime in the future.

If promises never existed, all our code that requires an external HTTP call will block the rest from executing until the call is over, just like our first waiter was stuck at the kitchen in between orders.

Concretely, on a website, you couldn't see the text or the shape until everyting has been loaded, leading to enormous loading time, or weird-looking pages. And nowadays, nobody want to wait more than 2 or 3 seconds for a website to load, right ?

Let's dig into the code

How can I use them ?

Now, let's write our first Promise. A very basic one would look like this :

new Promise((resolve, reject) => {
  // Your asynchronous code
});

A promise always take a function with two arguments : resolve and reject. When the promise must return the result, we call resolve with the results. If something wrong happened, let's say we're missing some ingredients, the whole promise is compromised, we must cancel the order and get something different from the client, this is where we call reject.

// Your promise can be stored in a variable, or returned within a function
const preparingDishes = new Promise((resolve, reject) => {
  const isIngredientMissing = false;

  const dishes = [
    // Everything that the client ordered, it would be filled up as soon as one is ready
  ];

  // But if an ingredient is missing, immedialty call back the waiter to inform the clients
  if (isIngredientMissing) return reject('An ingredient is missing !');

  // We use setTimeout to mimic an HTTP call
  setTimeout(() => {
    // 10 seconds later, once the dishes are ready, send them to the waiter
    return resolve(dishes);
  }, 10000);
});

Now, what is the value of preparingDishes ?

console.log(preparingDishes); // <---- What will appear in the console ?

If you're already familiar with promises, you answered "Promise {pending}" and you're right. If you were expecting [ ] don't worry, we'll figure this out.

console.log(preparingDishes); // <---- Promise {pending}

Why ? If we keep with our waiter exemple, we say to the kitchen : Hey, I have a new order, prepare it, and we didn't let the necessary preparation time, so the kitchen answer It's not ready ! We're still preparing it. The promise is pending.

How do we access the value then ?

Do you remember the MDN definition ? It allows you to associate handlers, the keywords here is handlers. Let's go back to our previous exemple, but let's get it to work for real this time.

const preparingDishes = new Promise((resolve, reject) => {
  // See the code above
});

// Now that our promise is created, let's trigger it, and then read the results
preparingDishes
  .then((dishes) => {
    // dishes is a arbitrary name, usually it's called result

    /* Within this function, we can access the result of the promise. The parameter will be the value you gave to the resolve.
    You are guaranted that anything you put in here, will execute when the promise is fullfilled (succesfull) */
    callWaiter(dishes);
  })
  .catch((error) => {
    // In case an error happened, this handler will catch the return value inside your above reject or any error that could happen in your promise code
    if (error === 'An ingredient is missing !') {
      sendWaiterBacktoClients();
    }
  })
  .finally(() => {
    // This one will execute anything that you put inside, either the promise succeed or not

    // For example, wether the kitchen succeed preparing the dishes or not, they'll have to start the next one
    prepareNextDishes();
  });

As you must have noticed by now, the .then, .catch and .finally are the handlers MDN is talking about. Each will execute under different circumstances as stated above. Please take note that attaching all the handlers isn't mandatory, you could only attach a .then for exemple (but I wouldn't recommended it), or only a .then and a .catch, which is what you'll use most of the time.

Nice ! Now that we write our first promise, and used it properly, let's go to the last part, where I personnaly struggled a lot with.

How can I recognize a Promise I didn't write ?

Let's say you're onboarding on a new project, you must get used to the codebase, but you're not sure what is asynchronous or not. Or even better, you're trying to figure out how to use a thrid-party library without the documentation.

You have 2 solutions to figure if a specific piece of code is a promise or not.

  • If you're using VSC as your text editor, you can let your mouse over the piece of code you're interested in. In the pop-up that's appearing, you can analyse what is the value. If this is a promise, you will see it as :

    Promise<any>
    

    Don't let the 'any' keyword instill doubt, this is some fancy Typescript, and will be replace with any value the promise is returning.

  • If a .then is hanging around, the variable before it is 100% a promise (unless the code you're stepping in is already broke ๐Ÿ˜•).

Additional informations

As we saw it, a promise is always followed by specific handlers. They are all meant to be used with promises, using them with a string for example will lead to errors where JS will complain that .then is not a function.

Even if promises are very cool, nesting promises in promises can lead to what we call the callback hell. Imagine that a specific piece of code result from a serie of promises, you'll have to wait the previous promises to be completed to access the value, and so on, leading to scary things like this :

gettingOrder.then((order) => {
  giveOrderToKitchen(order).then((dishes) => {
    serveClient(dishes).then(() => {
      startOver();
    });
  });
});

I purposely omitted all the .catch here, the readability already took a shoot. To solve this, we do have a powerful tool, async/await.

Async / Await

First, I would like to clarify something that took me a long time to understand, async/await is nothing more than syntactic sugar for promises.

Let's rewrite our first promise :

// Your function must be async to use the await keyword
async function waiterJob() {
  try {
    const dishes = await preparingDishes;
  } catch (error) {
    // Handle the error
  }
}

So, couple things changed here, we now have a function with the keyword async, a try/catch block, and the await keyword. Even if we still don't get what happened here yet, we can already say that it's way cleaner.

Here are the rules for using async/await :

  • The await keyword can only be used within an async function.

    • It replace the .then, you must not use it in conjuncution with .then/.catch. Exemple :
    // โ˜ ๏ธ Don't
    await preparingDishes.then((dishes) => {});
    
    // Don't do this kind of no sense either, it would work, but it's way more readable as a full async/await
    preparingDishes.then(async (dishes) => {
      await something();
    });
    
  • Making a function async will enforce it's return value to be a promise. In our exemple the waiterJob function is a promise that you'll have to await aswell when calling it.

  • Using await is a bit more tricky than the .then. If you want, you could await in front of a string, JS won't complain unlike the .then. Be very careful of not using it everywhere.
    • Using await where you don't need to won't lead to bug in itself, but the async function around it can, because it can break you app if you don't handle it properly.
  • To handle errors properly, you must wrap your promise call within a try/catch block. Our previous .catch will be handled here, and anything that break inside the try will be caught.

Hold on a second, you said that all async function must be awaited ? This is endless !

Well, not really. Most of the time in your real apps, you'll have a synchronous function upward which nobody depends on, like an initialization function.

If you don't have any, you could write what we call IIFE, it's basically a self-invoked function, allowing you to do this :

// How can I achieve this ?

import prepareTable from './';

const table = await prepareTable; // <---- Error, it's not wrapped within an async function

// ----------------------------------------

import prepareTable from './';

(async function () {
  const table = await prepareTable; // ๐Ÿ‘
})();

Here is what would look like our last example refactored with async/await

async function waiterJob() {
  try {
    const order = await gettingOrder();
    const dishes = await giveOrderToKitchen(order);
    await serveClient(dishes);
    startOver();
  } catch (error) {
    // Handle the error
  }
}

We'll wrap up here, we saw everything that will help you start with promises. If you are a more experienced developper and you think something is missing, please feel free to add a comment for it.

You can find the original article on the Othrys website and you can follow my Tweeter or tag me here to discuss about this article.

ย