How to deal with asynchronous code in JavaScript
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 => functionBfunction 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, initiallyundefined
.
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
tovalue
. reject(error)
— to indicate that an error occurred:- sets
state
to"rejected"
, - sets
result
toerror
.
resolve
andreject
— 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 ! :)