JavaScript

Let's take a look at how we can unlock the power that comes with async/await - by converting our own legacy code or other browser APIs which still rely on callbacks into Promise-ready functions.

I love async/await in modern JavaScript. We’re able to use async/await any time that a function returns a promise. Thankfully many libraries already support promises and many browser APIs such as Fetch support promises as well. Not all browser APIs have switched away from callbacks yet, but that doesn’t mean we can’t write a bit of code ourselves to convert them to support promises, therefore allowing us to use async/await where it wasn’t possible before.

In this article we’ll cover the conversion of callback code into promises using the Geolocation API, Image’s onload event, and the browser’s requestIdleCallback function.

I hope you will gain a better understanding of how promises work through these examples. All code found in this article can be found here.

Using a Promise

A Promise is an object that allows you to handle an asynchronous (future) event that will either provide a response or will fail with an error. Think of the Fetch API — it will either provide you the response of your AJAX request, or it will fail because it couldn’t connect to the server or any other 500 error. In both cases, you’re promised to either receive the positive result or the negative result, hence the name Promise.

We access the “positive” result of a Promise by calling the then function. An example using Fetch, taken from Mozilla’s documentation, ends up looking like this below. The keen eye will see two calls to then, meaning we have actually dealt with two different promises! The first when a response is received, and the second when the response was converted into JSON.

fetch("http://example.com/movies.json")
  .then(function(response) {
    return response.json();
  })
  .then(function(myJson) {
    console.log(JSON.stringify(myJson));
  });

Making Our Own Promise

We’re used to using other people’s promises, but let’s see what it looks like to write our own. The function passed to a new Promise is provided with two arguments: resolve, which is to be called when the code has completed successfully, and reject, which is to be called if there was an error.

const firstLetter = text => {
  return new Promise((resolve, reject) => {
    if (text && typeof text === "string") {
      resolve(text.charAt(0));
    } else {
      reject({
        error: `Must pass non-empty string but received "${text}" (${typeof text})`
      });
    }
  });
};

The code above, even though it is not asynchronous, returns us a promise that will either resolve to the first letter of the string passed in, or will be rejected with an error message if it fails. Let’s give our new promise a try:

() => {
  firstLetter("Leigh").then(letter => {
    console.log(letter);
  });
};

We can convert this code to use async/await by writing it like:

async () => {
  const letter = await firstLetter("Leigh");
  console.log(letter);
};

And we can test our code in Jest with:

import firstLetter from "./firstLetter";

it("finds first letter with resolves", () => {
  expect.assertions(1);
  expect(firstLetter("Leigh")).resolves.toBe("L");
});

it("finds first letter with async/await", async () => {
  const letter = await firstLetter("Leigh");
  expect(letter).toBe("L");
});

it("fails when null is passed", async () => {
  try {
    await firstLetter(5);
  } catch (e) {
    expect(e).toEqual({
      error: 'Must pass non-empty string but received "5" (number)'
    });
  }
});

Example: Image onload

Gatsby’s Image component comes with a neat ability to “blur up” the image. It starts out as a very blurry image immediately, and will be upgraded to the real image once the browser has finished loading it. We’re going to create a small version of this on our own, seeing how we can convert code with callbacks into code that uses promises (and async/await).

First things first, let’s see how we’d use the BlurImage component we’re building. We pass it the src to load along with a base64 “blurry” image to display until it can show the real one. I shrunk the image down to a max of 25px in size using the convert program (on a Mac) convert llama.jpg -resize 25x25 llama-small.jpg, and then used https://www.base64-image.de/ to extract the base64 code from that smaller image.

<BlurImage
  src="/images/llama.jpg"
  base64="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBAUEBAYFBQUGBgYHCQ4JCQgICRINDQoOFRIWFhUSFBQXGiEcFxgfGRQUHScdHyIjJSUlFhwpLCgkKyEkJST/2wBDAQYGBgkICREJCREkGBQYJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCT/wAARCAAOABkDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAABgcFCP/EACkQAAEEAQIEBQUAAAAAAAAAAAECAwQRBQAGEiEiURMxMmGRFRZVlNH/xAAWAQEBAQAAAAAAAAAAAAAAAAAEAAL/xAAcEQACAgMBAQAAAAAAAAAAAAABAgARAwUhkRP/2gAMAwEAAhEDEQA/ADr+6N0YQ47ITZuQUHULS0EzLAsilHmSOflfvpC1ltzsPQpb+UntxZq3yUSTSmw2gEhNA8Xn0nv5+4rc86W7icTIltxFB3jW0WmwhSRY9RAFm9a+FnLyeMTLl8Sk4uMsRmweXGs9a1dyQa0QZnu1Y+xDYkqmUeS0bK3VjMpEa8ZcuOwzxB2RkKUt+/SApNJBtQ6asiqGqB9Eg93PjXMiVoyO124PivoZmZeI2sCgUClG0nv0J+NKvs6P+WzP7R/mobT5cyMZk64P1FE//9k="
  alt="Beautiful llama"
  style={{ width: "250px" }}
/>

To do this we will use the Image object. It comes with an onload callback function that is called when the Image has finished loading the src it was provided with. Because we are wrapping a Promise around this code, we can call the resolve function of the promise once the image had loaded.

const loadImageWithPromise = src =>
  new Promise(resolve => {
    const image = new Image();
    image.onload = () => {
      resolve();
    };
    // or simplified to: image.onload = resolve;
    image.src = src;
  });

But what if it can’t load the image, or if it takes longer to load than we want it to? Let’s have our code trigger an error if it can’t load the image within a certain predefined period of time. If the image loads before the timeout occurs, it will clear the timeout. If the timeout occurs first, it will clear the onload callback.

const loadImageWithPromiseTimeout = src =>
  new Promise((resolve, reject) => {
    const image = new Image();

    const timeout = setTimeout(() => {
      image.onload = null;
      reject();
    }, 1000);

    image.onload = () => {
      clearTimeout(timeout);
      resolve();
    };

    image.src = src;
  });

Now we can use the above code within our BlurImage component:

awaitImage = async () => {
  try {
    await loadImageWithPromiseTimeout(this.props.src);
    this.setState({ loaded: true });
  } catch {
    console.error(`Unable to load ${this.props.src} in 1s`);
  }
};

The full component ends up looking like:

export default class BlurImage extends React.Component {
  state = {
    loaded: false
  };

  componentDidMount() {
    this.awaitImage();
  }

  awaitImage = async () => {
    try {
      await loadImageWithPromiseTimeout(this.props.src);
      this.setState({ loaded: true });
    } catch {
      console.error(`Unable to load ${this.props.src} in 1s`);
    }
  };

  render() {
    const { loaded } = this.state;
    const { src, base64, alt } = this.props;
    const currentSrc = loaded ? src : base64;

    return <img {...this.props} alt={alt} src={currentSrc} />;
  }
}

Example: Geolocation

The standard way that the geolocation.getCurrentPosition function works is by taking two callback functions as its arguments, one called on success and one called on error.

navigator.geolocation.getCurrentPosition(
  position => {
    console.log(position.coords);
  },
  error => {
    console.log(error);
  },
  options
);

What I’d really love is to have it work like this:

try {
  const position = await getCurrentPosition();
} catch (error) {
  console.log(error);
}

We’ll start off by declaring a getCurrentPosition function, which returns a new Promise. A Promise expects a resolve function, which is called on success, and a reject function, which is called when there is an error — very similar to the signature that the geolocation.getCurrentPosition has.

const getCurrentPosition = (options = {}) => {
  return new Promise((resolve, reject) => {
    navigator.geolocation.getCurrentPosition(resolve, reject, options);
  });
};

Now that we have created this helper function, we can call it using async/await.

loadLocation = async () => {
  try {
    const position = await getCurrentPosition();
    this.setState({ locationState: "SUCCESS", coords: position.coords });
  } catch (e) {
    this.setState({ locationState: "ERROR", error: e.message });
  }
};

Sweet! We’ve just converted a “callback” style function into one that returns a Promise and lets us use async/await. Here is the entire example below:

import React from "react";

const getCurrentPosition = (options = {}) => {
  return new Promise((resolve, reject) => {
    navigator.geolocation.getCurrentPosition(resolve, reject, options);
  });
};

export default class YourLocation extends React.Component {
  state = {
    locationState: "LOADING",
    error: null,
    coords: null
  };

  componentDidMount() {
    this.loadLocation();
  }

  loadLocation = async () => {
    try {
      const position = await getCurrentPosition();
      this.setState({ locationState: "SUCCESS", coords: position.coords });
    } catch (e) {
      this.setState({ locationState: "ERROR", error: e.message });
    }
  };

  render() {
    const { locationState, error, coords } = this.state;

    if (locationState === "LOADING") {
      return <div>Loading...</div>;
    }
    if (locationState === "ERROR") {
      return <div>{error}</div>;
    }

    return (
      <div>
        Latitude: {coords.latitude}, Longitude: {coords.longitude}
      </div>
    );
  }
}

Example: requestIdleCallback

The last example we’ll look at is the window.requestIdleCallback function. It allows us to perform non-important work when the browser is idle. requestIdleCallback will call our function when the browser is idle, which, in our example below, will resolve the Promise being returned.

const requestIdle = () =>
  new Promise(resolve => {
    window.requestIdleCallback(resolve);
  });

Now we can use this in our component:

componentDidMount() {
  this.onIdle();
}

onIdle = async () => {
  await requestIdle();
  console.log("I am idle");
};

Conclusion

In this article we investigated what a Promise is and how we can convert three different examples from code with callbacks into code that uses promises, allowing us to use the async/await language construct. While not all of the examples above provide an outright benefit over their callback counterparts, it’s important to be comfortable not just in using a Promise returned from a function that’s part of a library we’re using (or provided by the browser itself), but in creating our own promises.


For More Info on Building Great Web Apps

Want to learn more about creating great user interfaces? Check out Kendo UI  - our complete UI component library that allows you to quickly build high-quality, responsive apps. It includes all the components you’ll need, from grids and charts to schedulers and dials.


leigh-halliday
About the Author

Leigh Halliday

Leigh Halliday is a full-stack developer specializing in React and Ruby on Rails. He works for FlipGive, writes on his blog, and regularly posts coding tutorials on YouTube.

Related Posts

Comments

Comments are disabled in preview mode.