How to deal with asynchronous code in JavaScript

Tahere Gholami
Level Up Coding
Published in
5 min readMay 1, 2019

--

JavaScript is synchronous by default and is single threaded.
This means that code cannot create new threads and it will execute your code block by order after hoisting.

Programming languages like C, Java, C#, PHP, Go, Ruby, Swift and Python are all synchronous by default, some of them handle async by using threads and spawning a new process.

This is an example of a synchronous code

const a = 1;
console.log(a + 1);
console.log('3');
handleSomething();

Lines of code are executed in series, one after another.

Since JavaScript was born inside the browser, its main job, in the beginning, was to respond to user actions, like onClick, onMouseOver, onChange, onSubmit and so on. How could it do this with a synchronous programming model?

In some cases, for instance when you want to fetch some data from a server (which could take an unknown amount of time) it would be incredibly inefficient for your program to just freeze completely while it waited for that data to be fetched. So instead of doing that it’s common to just run the fetching task in the background.
This means that if you have two functions in a row with function A being asynchronous then function B will be executed while function A is still running. In this case if function B depends on data that function A is fetching you will run into problems.

Asynchronous means that things can happen independently of the main program flow.

Callbacks

This problem is solved with callbacks.

A callback is a simple function that’s passed as a value to another function, and will only be executed when the event happens.
With a callback you can guarantee that function B is only called after function A is finished with its thing because function A is actually the one responsible for calling function B.

// doSomething => functionA
// callback => functionB
function doSomething (options, callback) {
callback (options);
}
doSomething(options, callback);

Callbacks are great for simple cases, However every callback adds a level of nesting, and when you have lots of callbacks, the code starts to get complicated very quickly.

window.addEventListener('load', () => {
document.getElementById('button').addEventListener('click', () => {
setTimeout(() => {
items.forEach(item => {
//your code here
})
}, 2000)
})
})

This is just a simple 4-levels code, but you’ve seen much more levels of nesting and it’s not fun.

Starting with ES6, JavaScript introduced several features that help us with asynchronous code that do not involve using callbacks:

Promises

promises is one way to deal with callback dilemma and prevent writing too many callbacks in your code.

Once a promise has been called, it will start in pending state. This means that the caller function continues the execution, while it waits for the promise to do its own processing, and give the caller function some feedback.

At this point, the caller function waits for it to either return the promise in a resolved state, or in a rejected state, but the function continues its execution while the promise does it work.

The constructor syntax for a promise object is:

let promise = new Promise(function(resolve, reject) {
// executor (the producing code, "singer")
});

The resulting promise object has internal properties:

  • state — initially “pending”, then changes to either “fulfilled” or “rejected”,
  • result — an arbitrary value of your choosing, initially undefined.

When the executor finishes the job, it should call one of the functions that it gets as arguments:

  • resolve(value) — to indicate that the job finished successfully:
  • sets state to "fulfilled",
  • sets result to value.
  • reject(error) — to indicate that an error occurred:
  • sets state to "rejected",
  • sets result to error.

resolve and reject — these functions are pre-defined by the JavaScript engine. So we don’t need to create them. Instead, we should write the executor to call them when ready.

Here’s an example of a Promise constructor and a simple executor function :

let promise = new Promise(function(resolve, reject) {
// the function is executed automatically when the promise is constructed

// after 1 second signal that the job is done with the result "done"
setTimeout(() => resolve("done"), 1000);
});

After one second of “processing” the executor calls resolve("done") to produce the result:

That was an example of a successful job completion, a “fulfilled promise”.
And now lets look at an example of the executor rejecting the promise with an error:

let promise = new Promise(function(resolve, reject) {
// after 1 second signal that the job is finished with an error
setTimeout(() => reject(new Error("Whoops!")), 1000);
});

The executor should do something that usually takes time and then call resolve or reject to change the state of the corresponding Promise object.

promise can be consumed or used.

const doSomething = new Promise()
//...

const checkIfItsDone = () => {
doSomething
.then(ok => {
console.log(ok)
})
.catch(err => {
console.error(err)
})
}

Running checkIfItsDone() will execute the doSomething() promise and will wait for it to resolve, using the then callback, and if there is an error, it will handle it in the catch callback.

Promises were introduced to solve the famous callback dilemma, but they introduced complexity on their own, and syntax complexity.

Async/Await

Since ES2017 asynchronous JavaScript is even simpler with the async/await syntax.

Async Functions

In a more comfortable fashion, they reduce the boilerplate around promises, and the “don’t break the chain” limitation of chaining promises. It’s surprisingly easy to understand and use.

async keyword placed before a function and the keyword await makes JavaScript wait until that promise settles and returns its result.

async function f() {

let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});

let result = await promise; // wait till the promise resolves (*)

alert(result); // "done!"
}

f();

Prepending the async keyword to any function means that the function will return a promise and wraps non-promises in it. await literally makes JavaScript wait until the promise settles, and then go on with the result. That doesn’t cost any CPU resources, because the engine can do other jobs meanwhile: execute other scripts, handle events etc.

Here are some of my favorite blog posts on Promises and Async/Await coding, if you’re looking for more reading:

Thanks for reading ! :)

--

--