By default JavaScript is synchronous. The JavaScript engine maintains a stack data structure called the call stack
to keep track of function execution. And, as stacks go, the last item added to the stack is the first item to be manipulated.
When the JavaScript engine invokes a function, it adds it to the stack, and the execution starts. If the currently executed function calls another function, the engine adds the second function to the stack and starts executing it. Once it finishes executing the second function, the engine takes it out from the stack. The control goes back to resume the execution of the first function from the point it left it the last time. Once the execution of the first function is over, the engine takes it out of the stack. This continues until there is nothing left to put into the stack.
console.log("3");
console.log("2");
console.log("1");
console.log("Blast off!");
In the snippet above, each line executes in order. 2
will only log to the console after 3
has been logged and so on.
Defining asynchronous
Asynchronous allows a unit of work to run separately from the primary application sequence. In the context of JavaScript, this means code can execute out of sequence as shown in the example below.
console.log("3");
console.log("2");
setTimeout(() => {
console.log("1");
},1000)
console.log("Blast off!");
The setTimeout
callback function waits 1 second before executing. For this reason, the output will not log in sequence as expected but rather as:
3
2
Blast off!
1
Why would you use asynchronous code?
There are times when executing synchronously does not yield the intended result. Two categories of asynchronous operations fall under the following:
- Browser API and Web API (uses callback function)
- methods like
setTimeout()
- event handlers like
onClick()
- methods like
- Promises
Browser APIs like setTimeout and event handlers rely on callback functions. A callback function executes when an asynchronous operation completes.
In other words, event handlers do not follow the code sequence in order. The event handler is provided and will be called only when the event occurs (i.e. user clicks a button).
As for promises, the easiest way, in my opinion, to deal with them is to use async/await.
Async/Await
Introduced in ES 2017 (ES8), asynchronous functions make working with promises much easier as they build on top of promises. Prior to promises, we used callbacks.
An async function consists of two main keywords: async
and await
. The async
keyword is what makes the function asynchronous and will always return a promise regardless of what's inside of the function. The await
keyword signals the execution to wait until the defined task is executed.
const fetchPokemon = async (id) => {
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
const data = await res.json()
console.log(data);
};
fetchPokemon("pikachu");
- Line 2: waits for the response from the fetch request to the pokemon API.
- Line 3: waits for the response to be converted into json.
- Line 4: this converted data is then logged to the console when the
fetchPokemon()
function is invoked on line 7.
This looks great!
But what happens in the case where we do not pass an argument into the function like so fetchPokemon()
? This would throw an error but with our current implementation, we have no way to handle errors. To resolve this issue, two more keywords are introduced: try
and catch
.
try
consists of the code we want to be executed and catch
consists of handling any errors in the event the execution of our code fails.
const fetchPokemon = async (id) => {
try {
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
const data = await res.json();
console.log(data);
} catch (err) {
console.log(err);
}
};
fetchPokemon("pikachu");
So now, if the function is ran with no id
argument, the error is handled gracefully as its logged into the console.
Where to go on from here
Practice makes perfect. To become more familiar with writing asynchronous code, practice in a code sandbox or create a new project.