Understanding Callbacks, Promises, Generators and Async/Await
Muhammad Muhktar Musa
Posted on July 13, 2021
There are few fields in JavaScript that provide an equal amount of possible solutions and tools to the handling of asynchronous code. There is a reason for the existence of the vast amount of tools. Handling asynchronous code in a readable and manageable way has always been challenging in JavaScript. We are going to try to understand callbacks, promises, generators and async await so as to know when to use each tool and how they actually differ.
By default JavaScript is a synchronous language. This means it executes one line of code at a time and hence the need for certain approaches and tools to handle asynchronous code. Let us look at this approaches.
CALLBACKS
Callbacks are the oldest and simplest way to deal with asynchronous code in JavaScript. Callbacks are also known as higher order functions. They functions passed into functions and they are executed at some point. let us have a look at an example code
let x = function () {
console.log("i am called from inside a function");
};
let y = function (callback) {
console.log("do something here");
callback();
};
y(x);
In the above example we have a function x and another function y. Function y has an argument called callback and it is being executed as a function callback() within the function y as shown in the image below
When function y is executed it passes function x as an argument and it will execute inside function y as an argument and a callback.
Note that we are passing function x into function y as a function body. We are not passing the result of function x. We are passing the function body itself into another function and it would be executed at some point. Let us run the code and see what happens
When we run the code as in above, the first line of code in function y
console.log("do something here");
gets executed and the console prints the result. After this the function x gets executed printing to the console too. This is asynchronous behavior and a very simple way of executing callbacks.
The problem with callbacks is that when the application gets bigger, we have to do a lot of nesting which can lead to what is called callback hell . Error handling becomes difficult in this situation. Hence JavaScript gave us a solution called promises introduced in ES6
PROMISE
A promise in JavaScript is like a promise in real life. The promise has two outcomes, which are either the promise is resolved or the promise fails. Let us look at the syntax for creating a promise
We create a variable and set it to a new promise
let p = new Promise
This promise is an object and it takes a parameter which is a function. This function takes two parameters which are a resolve and a reject.
let p = new Promise((resolve, reject) => {
});
Then we need to create a definition of the function. We need to define what the actual promise is
let p = new Promise((resolve, reject) => {
let x = 2 + 2;
});
let x = 2 + 2; is what the promise does. If it resolves to true, we resolve the promise
let p = new Promise((resolve, reject) => {
let x = 2 + 2;
if (x == 4) {
resolve ('done') // we can pass in anything we want
}
});
if the promise does not resolve the promise rejects
let p = new Promise((resolve, reject) => {
let x = 2 + 2;
if (x == 4) {
resolve('done')
} else {
reject('error'); //we can also pass anything we want
}
});
The above code is always going to resolve because 2 + 2 = 4 is always going to resolve. If we change the code to be 2 + 3 which will give us 5, the code is going to reject because then x will not be equal to 4. let us now look at how we interact with a promise. Below the code block we can now say
p.then()
This is our promise and everything inside the
.then()
is going to run for a resolve. The
.then()
is going to take a single parameter and in our case it is going to be
p.then(message => {
});
We want to decide what we want to do with our message, thus we can pass a message to resolve the promise.
p.then(message => {
console.log("this is a message" + message);
});
To catch an error in a promise, we need to add the
.catch()
method to the promise. It will catch any error in our promise.
p.then(message => {
console.log("this is a message" + message);
}).catch(error => console.log('error'));
The promise is fulfilled from the above code.
This is exactly how a promise is used. They are very similar to callbacks but they are a little bit cleaner way of doing callbacks.
Promises are really great when something is to be done in the background. The error can also be caught if it fails and a message can be sent if it fails.
GENERATORS
A generator is a function that can be paused. This will allow the writing of code in an asynchronous fashion. Let us go straight to the syntax. After defining a function, an asteryx is added to the function keyword
function* car() {
}
values can now be yielded and stored into a variable
function* car() {
const variable = yield value;
};
Essentially, once that value is resolved or returned from whatever computations performed, it will be stored in a variable. The yield keyword can be used multiple times.
function* car() {
const numb2 = yield 2;
const numb3 = yield 3;
const numb4 = yield 4;
const numb5 = yield 5;
};
and so on. After defining the generator, it needs to be setup to be actually used. We do this by setting the function to a variable
const gen = car();
The function has been set and it is ready to get all values from the generator. To get the value from the generator we can use a series of methods like
gen.next()
gen.next().value
gen.next().done
next()
is an object. The object contains a property called values which represents whatever value that is yielded from the generator. The
next().done
is a Boolean that represents whether the generator has simply finished. Let us take an example in code
const getNumber = function* () {
yield 2;
yield "hello";
yield true;
yield { name: "anna" }
};
To use the above function we have created as a generator, assign it to a variable
const numberGen = getNumber();
If the code is is executed, nothing really will happen because we invoked the function
getNumber()
without traversing it line by line.
const numberGen = getNumber();
console.log(numberGen.next());
If the above code is executed, we will get an object.
The object shows that line one of the generator and that
next().done
is false. To get the whole value, we can duplicate the
next()
function a couple of times
const getNumber = function* () {
yield 2;
yield "hello";
yield true;
yield { name: "anna" }
};
const numberGen = getNumber();
console.log(numberGen.next());
console.log(numberGen.next());
console.log(numberGen.next());
console.log(numberGen.next());
You should see that we have a few objects and at the bottom we get a value done: true. which means that our generator has finished traversing the function.
To get the actual value of our generator, we simply append
.value;
to
.next()
method
console.log(numberGen.next().value);
and we get our values. To generate a value when done, simply add a return statement at the end of the function
const getNumber = function* () {
yield 2;
yield "hello";
yield true;
yield { name: "anna" };
return 'i am done'
};
const numberGen = getNumber();
console.log(numberGen.next().value);
console.log(numberGen.next().value);
console.log(numberGen.next().value);
console.log(numberGen.next().value);
console.log(numberGen.next().value);
Promises can be used along with generators. It is an interesting feature. We will leave that for another day
ASYNC/AWAIT
Async/await is JavaScript baking callbacks, promises and generators into a single function. Let us take a look at the async/await syntax
async function logName(name) {
console.log(name);
}
logName('Anna');
we get a name 'Anna'. now remove the async keyword from the code
function logName(name) {
console.log(name);
}
logName('Anna');
We still get the same exact response. We can see that when we have the async keyword appended to the function, we can yield promises inside the function body using the await keyword.
The second thing is that the function returns a promise. For example
async function logName(name) {
console.log(name);
}
logName('Anna').then(res => {
console.log('hello from me' + res);
});
As you can see we get a response which is a promise. If we remove the async keyword from the function, we most likely will get an error.
Using the async keyword let us go inside the function and create a new promise
async function logName(name) {
console.log(name);
const tranformName = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve(name.toUpperCase())
}, 2000);
return name
});
};
Now we can go ahead and use the promise. The way that we use the promise is we can use the keyword called await.
async function logName(name) {
//we can yield promises using await
const transformName = new Promise((resolve, reject) => {
setTimeout(() => resolve(name.toUpperCase()), 2000);
});
const result = await transformName;
console.log(result);
//it returns a promise
console.log(name);
return result;
};
This will return the actual result after the setTimeout method runs.
So basically those are the two things you need to know when using the async/await. Whatever is returned from the function ends up being a promise.
Posted on July 13, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.