DEV Community

Muhammad Muhktar Musa
Muhammad Muhktar Musa

Posted on • Updated on

Understanding Callbacks, Promises, Generators and Async/Await

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);
Enter fullscreen mode Exit fullscreen mode

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

image

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.

image

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

image

When we run the code as in above, the first line of code in function y

 console.log("do something here");
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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) => {

});

Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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
    }
});

Enter fullscreen mode Exit fullscreen mode

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
    }
});
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

This is our promise and everything inside the

.then()
Enter fullscreen mode Exit fullscreen mode

is going to run for a resolve. The

.then()
Enter fullscreen mode Exit fullscreen mode

is going to take a single parameter and in our case it is going to be

p.then(message => {

});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

To catch an error in a promise, we need to add the

.catch()
Enter fullscreen mode Exit fullscreen mode

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'));
Enter fullscreen mode Exit fullscreen mode

The promise is fulfilled from the above code.

image

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() {

}
Enter fullscreen mode Exit fullscreen mode

values can now be yielded and stored into a variable

function* car() {
    const variable = yield value;
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
next()
Enter fullscreen mode Exit fullscreen mode

is an object. The object contains a property called values which represents whatever value that is yielded from the generator. The

next().done
Enter fullscreen mode Exit fullscreen mode

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" }
};
Enter fullscreen mode Exit fullscreen mode

To use the above function we have created as a generator, assign it to a variable

const numberGen = getNumber();
Enter fullscreen mode Exit fullscreen mode

If the code is is executed, nothing really will happen because we invoked the function

getNumber()
Enter fullscreen mode Exit fullscreen mode

without traversing it line by line.

const numberGen = getNumber();
console.log(numberGen.next());
Enter fullscreen mode Exit fullscreen mode

If the above code is executed, we will get an object.

image

The object shows that line one of the generator and that

next().done
Enter fullscreen mode Exit fullscreen mode

is false. To get the whole value, we can duplicate the

next()
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

image

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;
Enter fullscreen mode Exit fullscreen mode

to

.next()
Enter fullscreen mode Exit fullscreen mode

method

console.log(numberGen.next().value);
Enter fullscreen mode Exit fullscreen mode

image

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);
Enter fullscreen mode Exit fullscreen mode

image

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');
Enter fullscreen mode Exit fullscreen mode

image

we get a name 'Anna'. now remove the async keyword from the code

 function logName(name) {
    console.log(name);
}
logName('Anna');
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

image

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.

image

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
    });
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

This will return the actual result after the setTimeout method runs.

image

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.

Top comments (0)