Async/Await, Promises and Callbacks in JavaScript

Jan 14, 2023
--- views
Asynchronous JavaScript - Banner Image

Before we start learning what JavaScript Asynchronous Programming is, let's have a look at JavaScript Synchronous Programming first.

1. Synchronous JavaScript

JavaScript by it's nature is a synchronous programming language and is single-threaded. This means that the code cannot run in parallel and cannot execute multiple tasks at the same time. The JavaScript compiler reads instructions line by line, and responds to each event in an order.

Consider this example:

const sayHello = (name) => {
    return `Hello, ${name}!`;
};

let name = 'Stoman';
const greet = sayHello(name);
console.log(greet); // Hello, Stoman!

The example above will be executed in the following order:

  1. We declare an arrow function sayHello() that takes a name parameter
  2. We define a name variable that stores a name, in our case Stoman
  3. We store the return value of the sayHello() function to greet, which takes a name parameter
  4. We log the greet value to the console

We clearly see what's happening here, everything gets executed in an order and the compiler reads our code line by line. This might be ok with a simple program like this, but imagine you are running a task that's going to take a long time to be executed, and in the same time, you need to see the result of some other events while you're waiting for the running task to be finished. You don't want to wait for one thing to get finished first so that you can see or do something else.

Or, let's say you're using Twitter to write something, definetly, you don't want to finish writing a tweet to get a new follow notification or a new message from your friend, you want to be able to get new notifications, new tweets, new messages and a lot more while you're writing a tweet.

That's exactly why Asynchronous Programming is a necessity.

2. Asynchronous JavaScript

Asynchronous programming is a programming technique that will allow your code to respond to multiple tasks at the same time. Or in other words, multiple tasks can be executed in parallel.

There are different ways that you can write asynchronous program in JavaScript, like the Async/Await keywords, Promises, and Callback functions. Let's start with Callback functions first.

2.1. Callback functions

JavaScript callback functions used to be the main way to write asynchronous code. A callback function is simply a function that can be passed into another function as an argument, and then, will be executed as soon as the other function is finished.

Look at this example:

const sayHello = (name, callback) => {
    console.log(`Hello, my name is ${name}.`);
    callback();
};

const introduceYourself = () => {
    console.log("I'm a software developer and design enthusiast.");
};

sayHello('Stoman', introduceYourself);
// Hello, my name is Stoman.
// I'm a software developer and design enthusiast.

Or this example:

const sayHello = () => {
    console.log('Hello, my name is Stoman');
};

const favouriteColor = (color) => {
    console.log(`My favuorite color is ${color}`);
};

setTimeout(sayHello, 2000);
favouriteColor('Black');
// My favuorite color is Black
// Hello, my name is Stoman

A slightly advanced example from the Nodejs documentation:

const final = (someInput, callback) => {
    callback(`${someInput} and terminated by executing callback `);
};

const middleware = (someInput, callback) => {
    return final(`${someInput} touched by middleware `, callback);
};

const initiate = () => {
    const someInput = 'hello this is a function ';
    middleware(someInput, function (result) {
        console.log(result);
        // requires callback to `return` result
    });
};

initiate();
// hello this is a function touched by middleware and terminated by executing callback

JavaScript callback functions are fine to be used for smaller code sizes, but imagine we want to have nested asynchronous functions to be executed one after another like this:

const sendTweet = (someParams) => {
    newMessage((someParams) => {
        newNotification((someParams) => {
            getTweetAnalytics((someParams) => {
                // Maybe many more nested functions here
            });
        });
    });
};

When having nested callback functions that must execute different tasks one after another, they become more and more complex to manage and read as your program grows in size and complexity. To make it more readable and easy to understand, we can use Promises instead of callback functions.

2.2. Promises

Promises introduced to the JavaScript language in ES6 (2015) with many other great features. A promise in JavaScript is simply a JavaScript object that will return a value in the future.

Here is how a promise in JavaScript looks like:

getTweets(someParams)
    .then( // Successful respone )
    .catch( // Rejected error );

A promise either returns a response or a reject error that can be executed within the .then and .catch blocks respectively.

const saySomething = new Promise((resolve, reject) => {
    setTimeout(() => resolve('I love Programming!'), 3000);
});

saySomething
    // Executes if there is a successful response
    .then((value) => {
        console.log(value);
    })
    // Executes if there is an error
    .catch((error) => {
        console.log(error);
    });

console.log('Log this statement firs!');
// Log this statement firs!
// I love Programming!

The .then() method takes two arguments; the first argument is a callback function for the successful response, and the second argument is a callback function for the rejected error. Each .then() returns a new promise that can be optionally used for chaining.

Look at this example:

const myPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('foo');
    }, 300);
});

myPromise
    .then(handleFulfilledA, handleRejectedA)
    .then(handleFulfilledB, handleRejectedB)
    .then(handleFulfilledC, handleRejectedC);

Look at this example where we fetch data from an API endpoint:

const getData = fetch('https://jsonplaceholder.typicode.com/posts/1');

getData
    .then((response) => {
        // Return the successful data
        return response.json();
    })
    .then((data) => {
        // If successful, log the data
        console.log(data);
    })
    .catch((error) => {
        // If error, log the error
        console.log(`Error: ${error.message}`);
    }).finally(
        const sayHello = () => {
            console.log('Logging from the finally statement.');
        }
    );

getData.then((data) => console.log('Log something else ...'));
/**
 * Output:
 * Log something else ...
 * (4) {userId: 1, id: 1, title: "sunt aut ...}
 */

For the exact same example above, we can also use some library to make it easy to fetch data, like axios:

npm install axios --save

const axios = require('axios');

axios.get('https://jsonplaceholder.typicode.com/posts/1');

There is also an optional finally() method that can be executed when the promise is either fulfilled or rejected.

const getData = fetch('https://jsonplaceholder.typicode.com/posts/1');

getData
    .then((response) => {
        // Return the successful data
        return response.json();
    })
    .then((data) => {
        // If successful, log the data
        console.log(data);
    })
    .catch((error) => {
        // If error, log the error
        console.log(`Error: ${error.message}`);
    })
    .finally(function sayHello() {
        console.log('Logging from the finally statement.');
    });

getData.then((data) => console.log('Log something else ...'));
/**
 * Output:
 * Log something else ...
 * (4) {userId: 1, id: 1, title: "sunt aut ...}
 * Logging from the finally statement.
 */

Promises are a great way to work with asynchronous code in JavaScript, but if you prefer a cleaner and more readable way to work with asynchronous code in JavaScript, the Async/Await keywords are nice to use. The Async/Await keywords are widely used nowadays in a lot of projects and are also the prefered choice for many programmer.

2.3. Async/Await keyword

Async/Await introduced into the JavaScript language in ES2017. The async/await keywords in JavaScript are a more comfortable way of using promises, or as they say, it is basically syntactic sugar for promises. An asynchronous function in JavaScript can be defined with the async keyword, similarly, the await keyword can be used to tell JavaScript to return the result of the promise instead of returning the promise itself.

In JavaScript, the async/await usage looks like this:

// 1. With normal functions
async function getTweets(someParams) {
    await doSomethingAsynchronously;
    // Or you can store the result to another variable
    const result = await doSomething;
}

// 2. With arrow functions
const getTweets = async () => {
    await doSomethingAsynchronously;
    // Or you can store the result to another variable
    const result = await doSomething;
};

The await keyword in an async function, blocks the program execution within that function untill there is a response or a rejected error.

Example:

console.log(1);
console.log(2);

const compareStrings = () => {
    const promise = new Promise((resolve, reject) => {
        const x = 'Programming';
        const y = 'Programming';
        x === y
            ? resolve('Strings are the same')
            : reject('Strings are not the same');
    });

    return promise;
};

const finalResult = async () => {
    try {
        let message = await compareStrings();
        console.log(message);
    } catch (error) {
        console.log('Error: ' + error);
    }
};

finalResult();

console.log(3);
console.log(4);

/**
 * Output:
 * 1
 * 2
 * 3
 * 4
 * Strings are the same
 */

As you can see, we don't stop the execution of the program. We log all statements while the await keyword stops the program exection until there is a response or an error.

Error handling in async/await:
const getData = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Successful response');
    }, 4000);
});

const finalResult = async () => {
    try {
        // Await the response
        let result = await getData;
        console.log(result);
    } catch (error) {
        console.log(error);
    }
};

finalResult();

console.log('Something else here!');
// Something else here!
// Successful response

The try catch block is a great way to easily handle any error in the program. The try block gets everything that needs to be executed in order to get a successful response, while the catch block takes care of any error that might occur in the program.

Conclusion

JavaScript is a single-threaded synchronous programming language, but with the help of promises and async/await keywords, JavaScript can be turned into an asynchronous programming language.

In this article we learned what Aynchronous JavaScrippt is, what are Callack functions, promise for better asynchronous programming, and then working with Async/Await that are a more practical and easy way to use promises. I hope this article makes it easier for you to understand Asynchronous JavaScript that you can apply it in your JavaScript/Node.js projects.

There is a lot more to the Asynchronous Programming in JavaScript that I didn't mention here like, Control Flow, Promise Constructors, Promise API, Microtasks and a lot more that you can learn through the links I have provided for you in the furhter readings section below.

Further readings