One of my favorite sites is BerkshireHathaway.com– it’s simple, effective, and has worked since it was launched in 1997. What’s even more remarkable is that the site has probably never had an error in 20 years. Why is that? Because it’s all static. It’s pretty much the same since it was introduced more than 20 years ago. Building a website is easy if you have all the data up front. Unfortunately, most websites don’t have it these days. To compensate for this, we invented “patterns” to handle extracting external data for our applications. Like most things, these patterns change over time. In this article, we’ll examine the pros and cons of the three most common patterns — Callbacks, Promises, and Async/Await — and talk about their significance and progress in a historical context.

So let’s start with the initial pattern that this data gets, Callbacks.

Callbacks

I’m assuming you have no idea what a callback is. If I’m wrong, just scroll down and skip.

When I first learned programming, it helped me understand functions as machines. These machines can do anything you want. They can even take input and return a value. Each machine has a button that you can press when you want the machine to run, namely ().

function add (x, y) {
  returnX + y} add(2,3) // 5 - press button to execute machineCopy the code

It doesn’t matter if I press the button, you press the button, or someone else presses the button. Whenever the button is pressed, the machine will run.

function add (x, y) {
  returnX + y} const me = add const you = add const someoneElse = add me(2,3) // 5-press the button Run the machine. You (2,3) // 5-press the button, run the machine. run the machine.Copy the code

In the code above, we assign the add function, three different variables, me, you, and someoneElse. It is important to note that add the original variables that we create and each variable point to the same location in memory. They are identical under different names. So, when we call me you, or someoneElse, it’s like we’re calling the add function. Now what if we send the ADD machine to another machine? Remember, it doesn’t matter if you press the () button, if you press it, it will run.

function add (x, y) {
  return x + y
}

function addFive (x, addReference) {
  return addReference(x, 5) // 15 - Press the button, run the machine.
}

addFive(10, add) // 15
Copy the code

Your brain may be a little weird at this point, but there’s nothing new here. Instead of “pressing the button” add, we pass addFive as a parameter, rename it addReference, and then we “press the button” or call it.

This highlights some important concepts of the JavaScript language. First, just as you can pass a string or number as an argument to a function, you can also pass a reference to the function as an argument. When this is done, the function passed as an argument is called a callback function, and the function that is passed the callback function is called a higher-order function.

Because the vocabulary is important, the code here is the same as the renamed variables to match the concept they demonstrate.

function add (x,y) {
  return x + y
}

function higherOrderFunction (x, callback) {
  return callback(x, 5)
}

higherOrderFunction(10, add)
Copy the code

This pattern should look familiar and ubiquitous. If you’ve ever used any JavaScript Array methods, you’ve already used callbacks. If you’ve ever used lodash, you’ve already used callbacks. If you’ve ever used jQuery, you’ve already used callbacks.

[1, 2, 3]. The map ((I) = > I + 5) _) filter ([1, 2, 3, 4], (n) = > n % 2 = = = 0). $('#btn').on('click', () =>
  console.log('Callbacks are everywhere'))Copy the code

In general, there are two common use cases for callbacks. First, let’s look at the.map and _.filter examples, which are great abstractions to flip one value into another. We say, “Hey, here’s an array and a function. Come on, give me a new value based on the function I gave you. The second, which we saw in the jQuery example, is to defer the execution of a function to a specific time. “Hey, here’s the function. Continue to call BTN every time it clicks on an element with an ID. This is the second use case we’ll be looking at, “delaying the execution of a function until a certain time.”

So far we’ve only looked at the synchronization example. As we discussed at the beginning of this article, most of the applications we build don’t have all the data we need up front. Instead, they need to capture external data as the user interacts with the application. We just saw how callbacks can be a great use case because they again allow you to “delay execution of a function until a certain time.” It doesn’t take much imagination to see how we can adapt this sentence to data extraction. Instead of deferring the execution of the function to a specific time, we can delay the execution of the function until we have the required data. This is probably the most popular example of the jQuery method: getJSON.

// updateUI and showError are irrelevant.
// Pretend they do what they sound like.

const id = 'tylermcginnis'

$.getJSON({
  url: `https://api.github.com/users/${id}`,
  success: updateUI,
  error: showError,
})
Copy the code

We can’t update the app UI until we get the user data. So what do we do? We say, “Hey, this is an object. If the request succeeds, go ahead and call SUCCESS and pass the user’s data. If not, go ahead and call Error and pass the error object. You don’t need to worry about what each method does, just make sure you call them when you should. This is a perfect demonstration of using asynchronous request callbacks.

At this point, we’ve seen what callbacks are and how they can be useful in both synchronous and asynchronous code. What we haven’t talked about yet is the dark side of the pullback. Look at the code below. Can you tell me what happened?

// updateUI, showError, and getLocationURL are irrelevant.
// Pretend they do what they sound like.

const id = 'tylermcginnis'

$("#btn").on("click", () => {
  $.getJSON({
    url: `https://api.github.com/users/${id}`,
    success: (user) => {
      $.getJSON({
        url: getLocationURL(user.location.split(', ')),
        success (weather) {
          updateUI({
            user,
            weather: weather.query.results
          })
        },
        error: showError,
      })
    },
    error: showError
  })
})
Copy the code

If you find it helpful, you can play the live version here.

Notice that we added some callback layers. First, let’s say that the initial AJAX request does not run until the BTN clicks on the element with the ID. After clicking the button, we make the first request. If this request succeeds, we make a second request. If the request succeeds, we will call the updateUI method for the data obtained from both requests. Whether or not you understand the code at first glance, it is objectively harder to read than previous code. Which brings us to the theme of “callback hell.”

As humans, it’s natural for us to think sequentially. When you nest callbacks within nested callbacks, it forces you to think beyond your natural way of thinking. Errors occur when there is a disconnect between the way your software reads and the way nature thinks.

Like most software solutions, a common way to make “callback hell” more consumable is to modularize your code.

function getUser(id, onSuccess, onFailure) {
  $.getJSON({
    url: `https://api.github.com/users/${id}`,
    success: onSuccess,
    error: onFailure
  })
}

function getWeather(user, onSuccess, onFailure) {
  $.getJSON({
    url: getLocationURL(user.location.split(', ')),
    success: onSuccess,
    error: onFailure,
  })
}

$("#btn").on("click", () => {
  getUser("tylermcginnis", (user) => {
    getWeather(user, (weather) => {
      updateUI({
        user,
        weather: weather.query.results
      })
    }, showError)
  }, showError)
})
Copy the code

If you find it helpful, you can play the live version here.

Ok, function names help us understand what’s going on, but are they objectively “better”? Not a lot. We just put a Band-aid on the readability of the callback hell. The problem remains that we naturally think sequentially, and even with the extra functionality, nested callbacks will take us out of sequential thinking.

The next callback is related to inversion of control. When you write a callback, assume that you are responsible for the callback program and will call it when (and only when) it should. In effect, you convert control of a program to another program. When you’re dealing with libraries like jQuery, Lodash, or even Vanilla JavaScript, it’s safe to assume that the callback function is called at the right time with the right parameters. However, for many third-party libraries, callbacks are the interface to how you interact with them. It makes perfect sense that third-party libraries, whether intentionally or accidentally, can break the way they interact with your callbacks.

function criticalFunction () {
  // It's critical that this function // gets called and with the correct // arguments. } thirdPartyLib(criticalFunction)Copy the code

Since you are not the caller of the criticalFunction, you can control the time and parameters of its invocation. Most of the time it’s not a problem, but when it does, it’s a big problem.

Promises

Have you ever made a reservation to a busy restaurant? When that happens, the restaurant needs a way to contact you when the table opens. Historically, when your table is ready, they just take your name and yell. Then, naturally, they decided to fantasize. One solution is that once the table opens, they’ll take your number and text you instead of taking your name. This allows you to go beyond yelling, but more importantly, it allows them to target your mobile ads whenever needed. Sound familiar? It should be! Well, maybe not. This is the metaphor for callbacks! Giving your number to a restaurant is like giving a callback function to a third-party service. You want a restaurant to text you when the table is open, just as you expect a third-party service to call on your functionality when and how to express it. Once your number or callback is in their hands, you lose all control.

Fortunately, there is another solution. A design that allows you to maintain all control. You may even have experienced this before – it’s the little buzz they give you. You know, this.

The buzzer is always in one of three different states – pending, fulfilled or rejected.

Pending is the default initial state. When they give you the buzzer, it’s in this state.

Fulfilled: the state of the buzzer when it is blinking and your desk is ready.

Rejected When there is a problem, the buzzer is in the state. Maybe the restaurant is closing, or they forgot someone is renting it out at night.

Also, it’s important to remember that you, the receiver of the buzzer, have all the control. If the buzzer goes into fulfilled, you can go to your table. If it is put into fulfilled and you want to ignore it, then cool, you can do that too. If it’s put in Rejected, that’s bad, but you can eat somewhere else. If nothing happens and it stays pending, you never eat, but you don’t actually have anything.

Now that you’re the owner of the restaurant buzzer, let’s apply this knowledge to something important.

If giving a restaurant your number is like giving them a callback, receiving this little thing is like receiving a so-called “Promise.”

As always, let’s start with why. Why do Promises exist? They exist to add complexity that makes asynchronous requests more manageable. Much like a buzzer, a Promise can be in one of three states: pending, fulfilled, or rejected. Unlike buzzers, they represent these states that represent the state of the restaurant table, they represent the state of the asynchronous request.

If the asynchronous request is still in progress, the Promise state is pending. If the asynchronous request completes successfully, the Promise state will change to fulfilled. If the asynchronous request fails, the Promise changes to the state Rejected. The buzzer metaphor makes sense, doesn’t it?

Now that you understand why promises exist and the different states in which they can exist, there are three more questions to answer. 1. How to create a Promise? 2. How to change the state of Prommise? 3. How to listen when a Promise’s state changes?

How do you create a Promise?

That’s pretty straightforward. Create a new instance, Promise.

const promise = new Promise()
Copy the code

How to change the state of Prommise?

The Promise constructor takes one argument and one (callback) function. This function will pass two arguments, resolve and reject.

Fulfilled – a feature that allows you to change the state of a Promise

Reject – a function rejected that allows you to change the Promise state.

In the code below, we wait 2 seconds with setTimeout and then call resolve. This will change the state of the Promise.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve() // Change status to 'fulfilled'}, 2000)})Copy the code

We can see this change by recording promise immediately after it is created, and then resolve again approximately 2 seconds after the call.

Note that Promise goes from pending to resolved.

How do I listen when a Promise’s state changes?

In my opinion, this is the most important question. It’s cool that we know how to create a Promise and change its state, but it’s worthless if we don’t know how to do anything after the state changes.

One thing we haven’t talked about yet is what Promise actually was. When you create a new Promise, you’re really just creating a plain old JavaScript object. The object can call two methods: THEN, and catch. That’s the key. When the state of a promise changes to fulfilled, the function passed to. Then will be called. When the state of the promise changes to Rejected,.catch calls the function passed to it. This means that once you create a promise, you will pass the function you want to run if the asynchronous request succeeds. If the asynchronous request fails, you pass the function to run. Catch.

Let’s look at an example. We will set setTimeout again using fulfilled to change the state of the Promise to after two seconds (2000 milliseconds).

function onSuccess () {
  console.log('Success! ')}function onError () {
  console.log('💩')
}

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve()
  }, 2000)
})

promise.then(onSuccess)
promise.catch(onError)
Copy the code

If you run the above code, you’ll notice that after about 2 seconds, you’ll see “Success!” in the console. . This is happening again because of two things. First, when we create a promise, we call resolve after ~ 2000 milliseconds – this changes the state of the promise to fulfilled. Second, we pass the onSuccess function to promises.then methods. By doing this, we tell the Promise that onSuccess is called when the Promise’s state changes to fulfilled ~ 2000 milliseconds later what it did.

Now let’s pretend that something bad has happened and we want to change the state of Promise. Resolve we call reject, not Reject.

function onSuccess () {
  console.log('Success! ')}function onError () {
  console.log('💩')
}

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject()
  }, 2000)
})

promise.then(onSuccess)
promise.catch(onError)
Copy the code

Now this time instead of onSuccess the onError function is called because we called the function reject.

Now that you’ve seen the methods of the Promise API, let’s start looking at some real code.

Remember that last example of an asynchronous callback we saw earlier?

function getUser(id, onSuccess, onFailure) {
  $.getJSON({
    url: `https://api.github.com/users/${id}`,
    success: onSuccess,
    error: onFailure
  })
}

function getWeather(user, onSuccess, onFailure) {
  $.getJSON({
    url: getLocationURL(user.location.split(', ')),
    success: onSuccess,
    error: onFailure,
  })
}

$("#btn").on("click", () => {
  getUser("tylermcginnis", (user) => {
    getWeather(user, (weather) => {
      updateUI({
        user,
        weather: weather.query.results
      })
    }, showError)
  }, showError)
})
Copy the code

Can we use the Promise API here instead of using callbacks? What if we include an AJAX request in a Promise? We can then simply resolve or reject depending on the way the request is made. Let’s get started with getUser.

function getUser(id) {
  return new Promise((resolve, reject) => {
    $.getJSON({
      url: `https://api.github.com/users/${id}`,
      success: resolve,
      error: reject
    })
  })
}
Copy the code

Very good. Notice that the getUser parameter has been changed. Instead of receiving the ID, onSuccess and onFailure, it just receives the ID. Those other two callbacks are no longer needed because we no longer reverse control. Instead, we use the Promise resolve and Reject functions. Resolve Reject is called if the request succeeds, and if there is an error.

Let’s refactor getWeather. We will follow the same strategy. Instead of receiving onSuccess and onFailure callbacks, we will use resolve and Reject.

function getWeather(user) {
  return new Promise((resolve, reject) => {
    $.getJSON({
      url: getLocationURL(user.location.split(', ')),
      success: resolve,
      error: reject,
    })
  })
}
Copy the code

It looks good. Now the last thing we need to update is our click handler. Remember, this is the process we want to take.

Get user information from the Github API. Use the user’s location to get their Weather from the Yahoo Weather API. 2. Update the UI with user information and its weather. 3. Let’s start with # 1 – getting user information from the Github API.

$("#btn").on("click", () => {
  const userPromise = getUser('tylermcginnis')

  userPromise.then((user) => {

  })

  userPromise.catch(showError)
})
Copy the code

Note that instead of getUser accepting two callbacks, it now returns a Promise that we can invoke with.then and.catch enabled. If.then is called, it will be called with the user’s information. If catch is called, it will be called with an error.

Next let’s do # 2 – use the user’s location to get their weather.

$("#btn").on("click", () => {
  const userPromise = getUser('tylermcginnis')

  userPromise.then((user) => {
    const weatherPromise = getWeather(user)
    weatherPromise.then((weather) => {

    })

    weatherPromise.catch(showError)
  })

  userPromise.catch(showError)
})
Copy the code

Note that we follow exactly the same pattern as we did in # 1, but now we call getWeather which passes our object userPromise to the user.

Finally, # 3 – Update the UI with user information and its weather.

$("#btn").on("click", () => {
  const userPromise = getUser('tylermcginnis')

  userPromise.then((user) => {
    const weatherPromise = getWeather(user)
    weatherPromise.then((weather) => {
      updateUI({
        user,
        weather: weather.query.results
      })
    })

    weatherPromise.catch(showError)
  })

  userPromise.catch(showError)
})
Copy the code

Here is the complete code you can use. The new code looks better, but we can still make some improvements. Before we can make these improvements, you need to know that Promise has two functions: chaining calls and passing arguments from resolve to THEN.

Chain calls

.then and.catch both return a new Promise. This may seem like a small detail, but it’s important because it means that promises can be chained.

In the following example, we call getPromise, which returns a Promise that will be resolved in at least 2000 milliseconds. From there, since.then will return a Promise, we can continue to link our.thens together until we throw a new Error that is caught by the.catch method.

function getPromise () {
  return new Promise((resolve) => {
    setTimeout(resolve, 2000)
  })
}

function logA () {
  console.log('A')}function logB () {
  console.log('B')}function logCAndThrow () {
  console.log('C')

  throw new Error()
}

function catchError () {
  console.log('Error! ')
}

getPromise()
  .then(logA) // A
  .then(logB) // B
  .then(logCAndThrow) // C
  .catch(catchError) // Error!
Copy the code

Cool, but why is it so important? Remember, in the callback section, we talked about one of the drawbacks of callbacks, which is that they force you out of a natural, sequential way of thinking. When you link promises together, it doesn’t force you out of that natural way of thinking, because chained calls to promises are continuous. getPromise runs then logA runs then logB runs then… .

So you can see another example, which is a common use case when using fetchAPI. The fetch returns a Promise that will be resolved through an HTTP response. To get the actual JSON, you need to call.json. Because of links, we can think about this in order.

fetch('/api/user.json')
  .then((response) => response.json())
  .then((user) => {
    // user is now ready to go.
  })
Copy the code

Now that we know about chained calls, let’s refactor our early getUser/ getWeather code and use it.

function getUser(id) {
  return new Promise((resolve, reject) => {
    $.getJSON({
      url: `https://api.github.com/users/${id}`,
      success: resolve,
      error: reject
    })
  })
}

function getWeather(user) {
  return new Promise((resolve, reject) => {
    $.getJSON({
      url: getLocationURL(user.location.split(', ')),
      success: resolve,
      error: reject,
    })
  })
}

$("#btn").on("click", () => {
  getUser("tylermcginnis")
    .then(getWeather)
    .then((weather) => {
      // We need both the user and the weather here.
      // Right now we just have the weather
      updateUI() // ????
    })
    .catch(showError)
})
Copy the code

It looks better, but now we have a problem. Can you spot it? In the second dot then we’re going to call updateUI. The problem is, we need to go through updateUI and weather. Currently how we set up, we only receive weather, not user. Somehow, we needed to figure out a way to make it a Promise, that is, getWeather uses user and to solve the return for weather.

That’s the key. Resolve is just a feature. Any arguments you pass to it will be passed to the given function.then. What that means is, inside getWeather, if we call resolve ourselves, we can pass weather and user through it. Then, the second method in our chain will take both user and weather as parameters.

function getWeather(user) {
  return new Promise((resolve, reject) => {
    $.getJSON({
      url: getLocationURL(user.location.split(', ')),
      success(weather) {
        resolve({ user, weather: weather.query.results })
      },
      error: reject,
    })
  })
}

$("#btn").on("click", () => {
  getUser("tylermcginnis")
    .then(getWeather)
    .then((data) => {
      // Now, data is an object with a
      // "weather" property and a "user" property.

      updateUI(data)
    })
    .catch(showError)
})
Copy the code

You can play with the final code here in our click handler, and you really see the power of a Promise compared to a callback.

/ / Callbacks 🚫 getUser ("tylermcginnis", (user) => { getWeather(user, (weather) => { updateUI({ user, weather: Weather.query.results})}, showError)}, showError) // Promises ✅ getUser(Promises ✅ getUser)"tylermcginnis")
  .then(getWeather)
  .then((data) => updateUI(data))
  .catch(showError);
Copy the code

It feels natural to follow this logic because it’s the way we’re used to thinking in order. GetUser then getWeather then Update the UI with the data. Now, it’s clear that promises will greatly improve the readability of asynchronous code, but are there ways to make it better? Suppose you are a member of the TC39 Committee and you have the ability to add new features to the JavaScript language. What steps will you take to improve this code?

$("#btn").on("click", () => {
  getUser("tylermcginnis")
    .then(getWeather)
    .then((data) => updateUI(data))
    .catch(showError)
})
Copy the code

As we discussed, the code reads very well. Just as our brain works, it is arranged in order. One of the problems we encountered was that we needed to move data (Users) from the first asynchronous request to the last.then. That’s not a big deal, but it lets us change our getWeather function and also pass it users. What if we write asynchronous code the same way we write synchronous code? If we did, the problem would disappear completely, and it would still read sequentially. That’s one idea.

$("#btn").on("click", () => {
  const user = getUser('tylermcginnis')
  const weather = getWeather(user)

  updateUI({
    user,
    weather,
  })
})
Copy the code

Well, that would be great. Our asynchronous code looks just like our synchronous code. There are no extra steps our brains need to take because we are already so familiar with this way of thinking. Sadly, this is clearly not going to work. As you know, if we were to run the code above, user and weather are both just promises, because that’s what getUser is and getWeather returns. But remember, we are using TC39. We have the ability to add any functionality we want to the language. As is, this code can be very tricky to make. We have to somehow teach the JavaScript engine to understand the difference between an asynchronous function call and a regular synchronous function call. Let’s add some keywords to the code to make things easier on the engine.

First, let’s add a keyword to the main function itself. This lets the engine know inside this function that we’re going to make some asynchronous function calls. Let’s use async for it.

$("#btn").on("click", async () => {
  const user = getUser('tylermcginnis')
  const weather = getWeather(user)

  updateUI({
    user,
    weather,
  })
})
Copy the code

Cool. That seems reasonable. Next, let’s add another keyword to let the engine know exactly when the called function is asynchronous and will return a promise. Let’s use await. Like, “Hey engine. This function is asynchronous and returns a promise. Instead of continuing as usual, continue to “wait” for the Promise’s final value and return it before continuing. With the two of our new async and await keywords in the game, our new code looks like this.

$("#btn").on("click", async () => {
  const user = await getUser('tylermcginnis')
  const weather = await getWeather(user.location)

  updateUI({
    user,
    weather,
  })
})
Copy the code

It’s very beautiful. We have developed a reasonable way to make our asynchronous code look and behave as if it were synchronous. Now the next step is to convince someone at TC39 that this is a good idea. Fortunately, as you may have guessed by now, we don’t need to do anything convincing because this functionality is already part of JavaScript and it’s called Async/Await.

Don’t believe me? This is our live code, now that we have added Async/Await. Play with it.

The asynchronous function returns a promise

Now that you’ve seen the benefits of Async/Await, let’s discuss some important details that are important. First, whenever async is added to a function, the function implicitly returns a promise.

async function getPromise(){}

const promise = getPromise()
Copy the code

Even if getPromise is literally empty, it still returns a Promise because it is an async function.

If the async function returns a value, that value will also be included in a promise. This means you will have to use.then it to access it.

async function add (x, y) {
  returnX + y} the add (2, 3). Then ((result) = > {the console. The log (result) / / 5})Copy the code

Waiting without asynchrony sucks

If you try to use await keyword async inside a non-function, you will get an error.

$("#btn").on("click", () => {
  const user = await getUser('tylermcginnis') // SyntaxError: await is a reserved word
  const weather = await getWeather(user.location) // SyntaxError: await is a reserved word

  updateUI({
    user,
    weather,
  })
})
Copy the code

Here’s my take on it. When async is added to a function, it performs two operations. It causes the function itself to return (or wrap the returned content) a Promise and makes it await for use within it

Error handling

You may have noticed that we cheated a little bit. In our original code, we have a way to catch any errors. When we switch to Async/Await, we remove this code. With Async/Await, the most common way is to wrap code in a try/catch block so that errors can be caught.

$("#btn").on("click", async () => {
  try {
    const user = await getUser('tylermcginnis')
    const weather = await getWeather(user.location)

    updateUI({
      user,
      weather,
    })
  } catch (e) {
    showError(e)
  }
})
Copy the code

(after)

link

  • Video address
  • The original link

Afterword.

The above translation is only used for learning communication, the level is limited, there are inevitably mistakes, please correct me.