Asynchronous Programming in JavaScript – Callbacks, Promises, & Async/Await Examples

Musab HabeebMusab Habeeb
5 min read

All programming languages have runtime engines that execute their code. In JavaScript, the runtime engine is single-threaded, which means that it runs code line by line or sequentially.

The JavaScript runtime engine makes it a synchronous programming language where programs run sequentially. Programming languages that are not synchronous are called asynchronous programming languages, which are programming languages where programs run concurrently.

Although JavaScript is synchronous, you can perform asynchronous programming with it. In this article, you will learn about asynchronous programming in JavaScript and how to use it.

What is Asynchronous Programming?

Asynchronous programming is a technique that allows your program to run its tasks concurrently. You can compare asynchronous programming to a chef with multiple cookers, pots, and kitchen utensils. This chef will be able to cook various dishes at a time.

Asynchronous programming makes your JavaScript programs run faster, and you can perform asynchronous programming with any of these:

  • Callbacks

  • Promises

  • Async/Await

In the upcoming sections, you will learn about these techniques and how to use them.

Callbacks

A callback is a function used as an argument in another function. Callbacks allow you to create asynchronous programs in JavaScript by passing the result of a function into another function.

function greet(name) {
    console.log(`Hi ${name}, how do you do?`);
}

function displayGreeting(callback) {
    let name = prompt("Please enter your name");
    callback(name);
};

displayGreeting(greet);

In the code above, the greet function is used to log a greeting to the console, and it needs the name of the person to be greeted.

The displayGreeting function gets the person's name and has a callback that passes the name as an argument to the greet function while calling it. Then the displayGreeting function is called with the greet function passed to it as an argument.

Callback hell

Although callbacks make it easy to control and make your program asynchronous, you'll eventually run into a problem called callback hell while using them.

This problem arises when you perform multiple asynchronous tasks with callbacks, which might result in nesting callbacks in callbacks.

Here's an example:

function greet(callback) {
    setTimeout(function() {
        console.log("Hi Musab");
        callback();
    }, 1000);
}

function introduce(callback) {
    setTimeout(function() {
        console.log("I am your academic advisor");
        callback();
    }, 1000);
}

function question(callback) {
    setTimeout(function() {
        console.log("Are you currently facing any challenge in your academics");;
        callback();
    }, 1000);
}

// callback hell
greet(function() {
    introduce(function() {
        question(function() {
            console.log("Done");
        });
    });
});

In the code above, the greet, introduce, and question functions are nested to create a callback hell, which makes error handling difficult. You should change your asynchronous programming technique from callbacks to Promise to avoid the callback hell.

Promise

Most programs consist of a producing code that performs a time-consuming task and a consuming code that needs the result of the producing code.

A Promise links the producing and the consuming code together. In the example below, the displayGreeting function is the producing code while the greet function is the consuming code.

let name;

// producing code
function displayGreeting(callback) {
    name = prompt("Please enter your name");
}

// consuming code
function greet(name) {
    console.log(`Hi ${name}, how do you do?`);
}

In the example below, the new Promise syntax creates a new Promise, which takes a function that executes the producing code. The function either resolves or rejects its task and assigns the Promise to a variable named promise.

If the producing code resolves, its result will be passed to the consuming code through the .then handler.

let name;

function displayGreeting() {
    name = prompt("Please enter your name");
}

let promise = new Promise(function(resolve, reject) {
    // the producing code
    displayGreeting();
    resolve(name)
});

function greet(result) {
    console.log(`Hi ${result}, how do you do?`);
}

promise.then(
    // the consuming code
    result => greet(result),
    error => alert(error)
);

You can convert the previous callback hell's example to a promise by returning a promise from each function and chaining the function calls together with the .then handler.

You can also use the .catch handler to catch any error thrown during the function execution.

function greet() {
    return new Promise(resolve => {
        setTimeout(function() {
            console.log("Hi Musab");
            resolve();
        }, 1000);
    });  
}

function introduce() {
    return new Promise(resolve => {
        setTimeout(function() {
            console.log("I am your academic advisor");
            resolve();
        }, 1000);
    });
}

function question() {
    return new Promise(resolve => {
        setTimeout(function() {
            console.log("Are you currently facing any challenge in your academics");;
            resolve();
        }, 1000);
    });
}

greet()
    .then(() => introduce())
    .then(() => question())
    .then(() => console.log("Done"))
    .catch(error => console.error("An error occured: ", error));

What are the States of a Promise in JavaScript?

A promise can be in any of these three states:

  • Pending: This is the initial state of the promise and its state while it's still running.

  • Fulfilled: This is the state of the promise when it resolves successfully.

  • Rejected: This is the state of the promise when errors make it not to be resolved.

Async/Await

async/await is syntactic sugar for creating a Promise — it makes creating promises easier.

To make a function asynchronous using async/await, you have to write the async keyword before the function declaration. Then, you can write the await keyword before the producing code's execution call.

Here's an example:

let name;

function displayGreeting() {
    name = prompt("Please enter your name");
    return name;
}

function greet(result) {
    console.log(`Hi ${result}, how do you do?`);
}

async function greeting() {
    // the producing code
    let result = await displayGreeting();
    // the consuming code
    greet(result);
};

greeting();

In the example above, the producing code is the displayGreeting function, and the consuming code is the greet function. The greeting function is the Promise that connects the producing and the consuming code. It waits for the result returned from the displayGreeting function and passes that result to the greet function.

Error Handling in Async/Await

You can easily handle errors that arise when you perform asynchronous operations with async/await using the try...catch statement. The asynchronous operation executes in the try block, and you can handle errors in the catch block.

That is:

async function greeting() {
    try {
        let result = await displayGreeting();
        greet(result);
    } catch(err) {
        console.error(err)
    }
};

Conclusion

Asynchronous programming in JavaScript is used to make tasks in a program run concurrently and uses techniques such as callbacks, Promise, or async/await.

This article explains how to use these asynchronous programming techniques and how to handle errors with them.

You can check the promises and async/await section on the JavaScript.info website to learn more about asynchronous programming in JavaScript.

0
Subscribe to my newsletter

Read articles from Musab Habeeb directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Musab Habeeb
Musab Habeeb