/ JavaScript

Callbacks vs. Promises vs. Async/Awaits

When writing or debugging code, we usually think of how the interpreter process that code is going from top to down of the whole code, line by line. Now, of course, that's not actually what happens in real life. Interpreters are far more complicated than that, but hypothetically we can think of that process this way.

That applies to the synchronous (or blocking) code, but it requires a bit of twist in the mindset when thinking of the asynchronous (or non-blocking) system. Taking that Node.js is a non-blocking environment, let's define the async operation and see the methods to deal with it in JavaScript and Node.js.

Difference Between Sync and Async Code

To simplify it, let's take an example from real life that is probably overly used to explain the difference.

When calling a customer service center, they are usually busy, and you would have to wait on the line for an agent to be free to start processing your request. You remain there for a couple of mins, and if you are lucky, you get an agent assigned to you after that.

However, in other customer service centers, they will take your name and phone number and will tell you that they will call you later on. You continue with your life and whatever you were doing until they call you to process your request.

Now, in the first center represents synchronous process because you had to wait until some resource (agent in this case) can proceed with you, while the second center represents asynchronous operation because you did not have to wait, they said they would call when someone is available to proceed with you. This is the single, most fundamental difference between sync and async processes.

One important thing to keep in mind is that the single-threaded event handling systems are usually implemented using an event or message queue. So when a program is being executed synchronously, the thread will wait until the first statement is finished to jump to the second one, while in asynchronous execution, even if the first one was not completed, the execution will continue.

Some examples of async code in JS and Node.js are when using setTimeout and setInterval, sending AJAX requests, and I/O operations.

There are different ways to handle async code. Those are callbacks, promises, and ES2017's async/await.

Callbacks

Callbacks are just the name of a convention for using JavaScript functions. Instead of immediately returning some result like most functions, functions that use callbacks take some time to produce a result. Let's take an example.

console.log("One"); 

setTimeout(() => { console.log("Two"); }, 500); 

console.log("Three");

If that were sync code, we would have encountered the following output.

One
Two
Three

However, because setTimeout is an async function, we will have the following output.

One
Three
Two

The callback is the function that is passed as the first argument to setTimeout, and it will be executed after passing the 500 milliseconds.

Node.js was built on that idea, and it makes the foundation of it.

One of the drawbacks of callbacks is what is known as the callback hell. It happens when we have a callback, inside of a callback, inside of a callback, etc. Basically bunch of nested callbacks. Here is an example from callbackhell.com.

fs.readdir(source, function (err, files) {
    if (err) {
        console.log('Error finding files: ' + err);
    } else {
        files.forEach(function (filename, fileIndex) {
            console.log(filename);
            gm(source + filename).size(function (err, values) {
                if (err) {
                    console.log('Error identifying file size: ' + err);
                } else {
                    console.log(filename + ' : ' + values);
                    aspect = (values.width / values.height);
                    widths.forEach(function (width, widthIndex) {
                    height = Math.round(width / aspect);
                    console.log('resizing ' + filename + 'to ' + height + 'x' + height);
                    this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
                        if (err) console.log('Error writing file: ' + err);
                    })
                    }.bind(this));
                }
            });
        });
    }
})

Promises came to the rescue.

Promises

The Promise object represents the possible completion (or failure) of an asynchronous operation and its resulting value. It is a proxy for a value not necessarily known at its creation time, and it represents the future result of an asynchronous operation.

The calling code can wait until that promise is fulfilled before executing the next step. To do so, the promise has a method named then, which accepts a function that will be invoked when the promise has been fulfilled.

Promises became native in JavaScript since ES2015, but the idea of the Promises was there way before.

The syntax looks like the following.

new Promise(function(resolve, reject) { ... } );

Let's take an example.

let myFirstPromise = new Promise((resolve, reject) => {
    setTimeout(function(){
        resolve("Success!"); // fulfilled
    }, 250);
});

myFirstPromise.then((successMessage) => {
    console.log("Yay! " + successMessage);
});

Here we define a Promise, myFirstPromise, which will take an arrow function as the executor. The setTimeout(...) is to simulate async code which could be something like an xhr request. We call resolve(...) when what we were doing asynchronously was successful, and reject(...) when it failed. successMessage in then(...) is whatever we passed in the resolve(...) function above, and it doesn't have to be a string, but if it is only a succeed message, it probably will be.

You can find more about ES2015 promises here.

ES2017's Async/Await

Promises were a lot better than callbacks and solved the problems that the callbacks introduced but still had this type of unnatural way of dealing with async code. Just to have async/await which let the developer deal with async code naturally and with no gimmicks.

When an async function is called, it returns a Promise. When the async function returns a value, the Promise will be resolved with the returned value. When the async function throws an exception or some value, the Promise will be rejected with the thrown value.

An async function can contain an await expression, which pauses the execution of the async function and waits for the passed Promise's resolution, and then resumes the async function's execution and returns the resolved value. Let's take an example.

function resolveAfter2Seconds(x) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(x);
        }, 2000);
    });
}


async function add1(x) {
    const a = await resolveAfter2Seconds(20);
    const b = await resolveAfter2Seconds(30);
    return x + a + b;
}

add1(10).then(v => {
    console.log(v); // prints 60 after 4 seconds.
});


async function add2(x) {
    const p_a = resolveAfter2Seconds(20);
    const p_b = resolveAfter2Seconds(30);
    return x + await p_a + await p_b;
}

add2(10).then(v => {
    console.log(v); // prints 60 after 2 seconds.
});

You can read more about async/await here.

Anas Shekhamis

Anas Shekhamis

A software engineer who specializes in architecting and developing web applications. On a daily basis, I use Python, JavaScript, Ruby, and PHP to design and implement API's and build web applications.

Read More
Callbacks vs. Promises vs. Async/Awaits
Share this