Asynchronous Programming: Callbacks, Promises, Async, Await

Asynchronous Programming: Callbacks, Promises, Async, Await

In JavaScript, you oftentimes need to perform tasks that will take an undetermined amount of time. Because of this, you want to allow your code to continue running instead of waiting until the task completes. This is called asynchronous programming.

Callback Functions

One of the most popular ways to employ asynchronous programming is through the use of callback functions. As the name implies, a callback function is a function that is called after another function has completed. Let's look at a simple example:

	
    function doChore(chore, callback) {
        alert("Started " + chore + ".");
        callback();
    }

    function finished() {
        alert("Finished my chore!");
    }

    doChore("cleaning", finished);
	

Here you have a function doChore, that takes two parameters, the name of the chore you are doing, and then a function that it will call after the chore is done. We are passing a function that simply alerts the user that the chore is complete, the finished() function. As you would expect, this is the end result:

	
    alert("Started cleaning.");
    alert("Finished my chore!");
	

In reality, anything can happen inside doChore before the callback function is called. Maybe you need to make a request to another server or read a file from disk. The point to get out of this is that the callback function is only called after the rest of function is finished.

We can see this better illustrated when we use the setTimeout function which can add a delay to our code:

	
    function doChore(chore, callback) {
        alert("Started " + chore + ".");
        setTimeout(callback, 3000);
    }

    function finished() {
        alert("Finished my chore after 3 seconds!");
    }

    doChore("cleaning", finished);
	
	
    alert("Started cleaning.");
    alert("Finished my chore after 3 seconds!");
	

Promises

Promises are used to handle the results of an asynchronous operation, for example like making an API call or reading from disk. We want to continue to allow the code to continue to run and promises are a structured way to handle asynchronous operations.

Creating a Promise

Creating a promise is easy, simply create it like you would any object. It takes a callback function with two parameters, a resolve function and a reject function.

	
    const promise = new Promise((resolve, reject) => {
        // implementation here
    });
	

Promises only have three possible states they can be in:

  • Pending: A pending Promise hasn't begun its operations yet.
  • Fulfilled: When a promise resolves after its operations finished.
  • Rejected: When a promise fails to complete its operations successfully.

Let's look at an example promise mocking an API call:

	
    const success = true;
    const data = new Promise((resolve, reject) => {
        if (success) {
            const results = {
                message: "This was a success!"
            };

            resolve(results);
        } else {
            const results = {
                message: "This failed!"
            };

            reject(results);
        }
    });
	

In this bare-bones example, we are controlling whether or not this mock API data call succeeded with a boolean. Depending on that value, it will determine whether the promise resolves or is rejected.

Using Promises

Now that we have defined our promise, let's try actually using it:

	
    const success = true;
    const data = new Promise((resolve, reject) => {
        if (success) {
            const results = {
                message: "This was a success!"
            };

            resolve(results);
        } else {
            const results = {
                message: "This failed!"
            };

            reject(results);
        }
    });

    data.then(success => {
        console.log(success.message);
    }).catch(error => {
        console.log(error.message);
    })
	
	
    This was a success!
	

When you use the then function, it receives the value passed in to the resolve function. Likewise, the catch function receives the value passed into the reject function. That is why we are able to immediately access the message value that we passed in earlier.

Async and Await

Async and await are keywords in JavaScript that allow you to write promises in an easier and more visually appealing manner. Let's start by declaring a normal function as asynchronous.

	
    async function apiSuccess() {
        const results = {
            message: "This was a success!"
        };

        return results;
    }
	

The above code is the equivalent to this:

	
    function apiSuccess() {
        const results = {
            message: "This was a success!"
        };

        return Promise.resolve(results);
    }
	

Both of the above functions will resolve to the same exact thing. On the flipside, you can do the same in the reverse:

	
    async function apiFailure() {
        const results = {
            message: "This failed!"
        };

        throw new Error(results);
    }
	

That is equivalent to doing this:

	
    function apiFailure() {
        const results = {
            message: "This failed!"
        };

        return Promise.reject(results);
    }
	

Await

The await keyword is used with asynchronous functions to ensure that all the promises are completed and synchronized. This can help remove the need to use callbacks via .then() and .catch(). Here's an example:

	
    async function getAPIData(url) {
        try {
            let response = await fetch(url);
            let data = await response.json();

            console.log(data);
        } catch(error) {
            // catches errors in all async functions
            console.log(error);
        }
    }

    let url = "https://jsonplaceholder.typicode.com/todos/1";
    getAPIData(url);
	
	
    {
        userId: 1,
        id: 1,
        title: "delectus aut autem",
        completed: false
    }
	

Our asynchronous API call was successful and we got our data! Alternatively, if you simply wanted to return the data instead of print it out, you can write it like this:

	
    async function getAPIData(url) {
        try {
            let response = await fetch(url);
            let data = await response.json();

            return data;
        } catch(error) {
            return error;
        }
    }

    let url = "https://jsonplaceholder.typicode.com/todos/1";
    let data = await getAPIData(url);

    console.log(data);
	
	
    {
        userId: 1,
        id: 1,
        title: "delectus aut autem",
        completed: false
    }
	

Finally, if you want to call an async function from a non-async function, you can use the then method. Here's an example:

	
    async function getAPIData(url) {
        try {
            let response = await fetch(url);
            let data = await response.json();

            return data;
        } catch(error) {
            return error;
        }
    }

    function printAPIData(url) {
        getAPIData(url).then(result => {
            console.log(result);
        });
    }

    let url = "https://jsonplaceholder.typicode.com/todos/1";
    printAPIData(url);
	
	
    {
        userId: 1,
        id: 1,
        title: "delectus aut autem",
        completed: false
    }
	

Conclusion

Hopefully you have seen how useful JavaScripts asynchronous features can be. They allow you to rewrite code that previously were constant callbacks into something much more readable and semantic.

Resources