How to use Callbacks, Promises, and Async/Await in JS

Dilshan Hiruna
8 min readFeb 25, 2022

Let’s start the discussion by getting a proper understanding of synchronous and asynchronous in JavaScript. Why? because it will help you to get these concepts very easily.

Synchronous and Asynchronous

Basically, synchronous means sequentially. That means one task is executed only after the previous task is completed. And if there are n number of tasks, the nth task will begin the process only after the n-1th task is completed. And there is only one single task processing at a time. No 2 or multiple threads work parallelly.

Let’s consider the below example

console.log("task 01");
console.log("task 02");
console.log("task 03");
console.log("task 04");

If you are a programmer, you may already know what’s going to print in the console. Yes, the output will be;

task 01
task 02
task 03
task 04

And if you run this 100 times, the output will be in the same order. This is because JavaScript is by default synchronous or single-threaded and this means one task at a time. In this scenario, every line of code is executed one after the other.

Got it? Let’s have a look at how an asynchronous task works.

console.log("task 01");setTimeout(() => {
console.log("task 02");
}, 2000);
console.log("task 03");
console.log("task 04");

In the above example, I have used a setTimeout() method. A setTimeout() method takes two arguments, the first is a function and the second is the time which the function should be executed after. So in this example, the “task 02” will be printed after 2000ms or 2s.

Now, let's take a look at the output

task 01
task 03
task 04
task 02

As in the previous example, “task 01” was printed at first. But “task 02” was printed at last.

Although the “task 02” is printing 2s later the print statements below will not be blocked or wait till it completes its execution.

What are Callbacks functions and how to use them?

As you understand, for now, JavaScript is synchronous by default, which means it executes the source code in a sequential manner or one after another. But in some situations, we might need to run a specific function right after some other task’s execution completes.

A callback function refers to a function that has been passed to another function as an argument, which is executed inside the outer function.

By using this procedure, we can ensure that a function is invoked when it needed to or after some tasks are completed.

const func1= function() {  
console.log("This function 1");
}
const func2= function(callback_func) {
console.log("This function 2");
callback_func();
}
func2(func1);

In the above example, func2 takes func1 as an argument and invokes it right after the print statement is executed.

Now let's take a look at the setTimeout() method from the above synchronous and asynchronous example.

// ES6
setTimeout(() => {
console.log("task 02");
}, 2000);

Probably you might have understood that the setTimeout() method is an asynchronous function that takes a function as a callback by now. The function that has been passed as an argument, executes only after a given period of time.

And if you are confused with ES6 arrow functions, it is similar to below

// ES5
setTimeout(function() {
console.log("task 02");
}, 2000);

Now, let’s build a chain of functions that executes one after the other.

setTimeout(() => {
console.log("task 01");
setTimeout(() => {
console.log("task 02");
setTimeout(() => {
console.log("task 03");
setTimeout(() => {
console.log("task 04");
}, 1000);
}, 1000);
}, 1000);
}, 1000);

output:

task 01 //printed after 1s
task 02 //printed after 2s
task 03 //printed after 3s
task 04 //printed after 4s

These print statements will be executed one by one after 1s each. But did you see an issue in here?

Although it is easy to manage a single callback function, the code is getting more complicated and messed up while chaining multiple functions together. This is known as callback hell. This is a drawback of callback functions.

But we have a solution for that. Promises!

Promises

Ummm… The image might not be super relevant here, but it’s kinda like that.

JavaScript promises are similar to the promises that we all know. It will promise that it will return something in the future.

But how does it differ from the callback functions that we talked about earlier?

A promise is a returned object to which you attach callback functions, instead of passing callbacks into a function. The place where you attach the callback after execution of a task is called, .then().

By using promises, we can chain multiple async functions together using .then() operation passing the returned result of the previous function to the new function. But the multiple chaining will not cause a callback hell as with the callback functions.

Promises allow you to catch all the errors in a single .catch() at the end of all the chained blocks. This makes it easier for error handling rather than handling errors inside each function as of callback functions.

Callbacks approach is “Do this and after doing that”

Promises approach is “Do something using the output of that”

How do promises work?

Promises are moved into an event queue, which will be run after the main thread has finished its execution of tasks. And this allows the synchronous operation to continue with the process sequentially. After the completion of the asynchronous operations finishes their task the results will be returned to the environment.

A promise has three stages:

  • Pending: This is the initial stage
  • Resolved: This means, we got what we needed, the operation was completed successfully
  • Rejected: This means, things did not go well, the operation failed!

Now, let’s dive into the implementation.

const promise = new Promise((resolve, reject) => {
let works = true;
if (works) {
resolve("Success");
} else {
reject("Error");
}
});
promise.
then((value) => {
console.log(value); //Success
}).
catch((value) => {
console.log(value);
});

A promise can either of resolve() or reject(). But anyway, right after either one of those invoked, the .then() will be called.

.then() takes 2 functional arguments. One for handling success and the other for error handling.

//will be invoked if resolved or rejected
.then((result) => {
// success
}, (error) => {
// error
})

.catch() will only take one functional argument as a parameter. And that’s for the error. And that makes sense because the purpose of the .catch() is to handle errors.

//will be invoked if rejected
.catch((error) => {
// error
})

Although .catch() is a separate handler, it uses .then(null, error) for error handling. So, if you are using .catch() it’s actually .then(null, error)

But there is another handler that is used rarely. The .finally() handler. This handler invokes whether the promise is rejected or resolved.

then((value) => {
// for Success
}).
catch((value) => {
// for error
}).
finally(() => {
// anything here
})

Chaining Promises

The following example is the usage of promise chaining.

const PowerOf2 = (val) => new Promise((resolve, reject) => {
setTimeout(() => {
resolve(val * val);
}, 1000);
});
const SquareRoot = (val) => new Promise((resolve, reject) => {
setTimeout(() => {
resolve(Math.sqrt(val));
}, 1000);
});
PowerOf2(8)
.then((val) => { // val = returned resolved value from PowerOf2
console.log(val) // 64
return SquareRoot(val) // using 64 as input
})
.then((val) => { // val = returned resolved value from SquareRoot
console.log(val) //8
})

output:

64
8

In the above example,

we are calling the function PowerOf2() by passing a value of 8 and then it returns the power of its passed value in the first .then() handler, which is 64.

And we have used that returned value (64) as the parameter for our next function SquareRoot(), which calculates the square root.

SquareRoot() returns the resolves value as same as the PowerOf2() used to do. And finally, we are printing out the value returned from the SquareRoot() in the second .then() handler, which is 8.

If you want to wait till all the promises to be completed to start a task, you can use Promise.all().

Promise.all() will take an array of promises and returns an array correspondingly.

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 1000, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values); // output: [3, 42, "foo"]
});

Although promise1 and promise2 resolve immediately, you have to wait until promise3 gets resolved to get the result of all promises.

Async / Await

Async functions are a better way to define promises while keeping the code clean and simple. Adding two keywords which are async and await to a regular function when declaring make the function asynchronous. The async expression lies with the function keyword while the await keyword declares inside the function.

The async expression is used as below

// ES5
async function func() {
return 1;
}
// ES6
const func = async ()=>{
return 1;
}
//funciton call
func().then((val)=>{
console.log(val) //output: 1
})

async ensures that the function returns a promise and await expression tells JS to wait until the promise returns something. But remember that, you can only use await keyword inside an async function.

async function f() { // ensures that function returns a promise by async
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // wait until the promise resolves
return result;
}
f().then((val)=>{
console.log(val); //output: done!
})

By using async / await, JS allows us to include try-catch blocks inside asynchronous functions.

async function f() {  try {
let response = await fetch('<http://no-such-url>');
} catch(err) {
console.log(err); // TypeError: failed to fetch
}
}

Conclusion

In this article, you have learned in detail how to use callbacks, promises, and async/await in JavaScript. Getting the basic knowledge of synchronous and asynchronous, differences and how do they work beforehand might have helped you in getting these concepts very easily.

Right after getting the basic knowledge on sync / async, we have gone through the callback functions and how to use them.

Before getting to promises, we have shown you the difference between callbacks and promises and when to use what.

Async / Await comes at last, which makes it easier to understand because you have the knowledge of promises.

Hope this article might have helped you. Thanks ❤️

--

--