Asynchronous Code in JavaScript: How It Works

Intro

Normally, JavaScript is a synchronous language. This means that code is ran from top to bottom, one line at a time. No two bits of code can run concurrently, and if a function takes a while to return, nothing can run in the background. This isn't the best for a number of reasons, with one of them being performance. Imagine if only one thing in, say, Facebook could run at a given moment. The page couldn't refresh without having to stop whatever it is you are doing, and button clicks wouldn't work without pausing the entire site. However, there is a solution. You already read the title, it's asynchronous code. This way of programming allows two code snippets to execute at the same time. For example, a button can be clicked on a webpage, which can execute a code function. The thing is, the website can still run while waiting for button presses.

Types of Asynchronous Code

There are generally three ways of writing asynchronous code in JavaScript. In order from least to most abstract, they are: - Callbacks - Promises - Async/Await Let's start with callbacks.

Callbacks

In its simplest form, a callback function is just a function that is ran when another function needs it to run, normally after it has finished executing. Take this code snippet for example: var doAThing = function (someInput, someCallback) { console.log("First execution:", someInput); someCallback(someInput); }; var anotherThing = function(someInput) { console.log("Second execution:", someInput); }; doAThing("Hello World!", anotherThing); This code will call doAThing with "Hello World" as an argument, which in turn will call anotherThing, being given "Hello World" as an argument. It will output: First execution: Hello World! Second execution: Hello World! Callbacks can also be used with anonymous functions. Here is another snippet, continuing on from the previous: doAThing("OwO", function(input) { if (input === "OwO") { console.log("UwU"); } else { console.log("Second execution:", input); } }); This will output: First execution: Hello World! Second execution: Hello World! OwO UwU This syntax is used with things like setTimeout, jQuery events, and Express.js routes. Callbacks are mainly used with something called the error first pattern, where the first argument is always an error. This allows for error catching. However, using callbacks can result in something called "callback hell", where callbacks are nested in each other ad infinitum, causing a massive pyramid of unreadable spaghetti code. doAThing(input, function (output) { doAnotherThing(output, function (putout) { doAThirdThing(putout, function (putput) { //Continues on... }); }); }); To avoid this, one can use promises.

Promises

Promises are, essentially, objects that represent future values. They start as pending, and can be resolved or rejected, depending on if there is an error. Promises can also be chained using .then, which takes in a callback and returns a promise, and .catch, used for catching errors. Take this code snippet: var doAThing = function (someInput) { return new Promise(function(resolve, reject) { console.log("First:", someInput); resolve(someInput); }); }; doAThing("Hello World!").then(function(next) { console.log("heyyyyyyy"); return next; }).then(function(next) { console.log(next); }); This will output: First: Hello World! heyyyyyyy Hello World! Do you notice how the .thens don't get worse indents as the chain propagates? This is one of the main features of promises. Promises can also use Promise.all, which essentially takes in a bunch of promises, and allows you to perform operations on their results.

Async and Await

Finally, we will talk about async/await. Essentially, async/await is syntactical sugar for promises. It allows you to await completion of a promise and assign values that would have been in a .then callback. var doAThing = function (someInput) { return new Promise(function(resolve, reject) { if (someInput === "OwO" ) { reject("No OwOs in the chat"); } else { resolve(someInput + "!!1111!!"); } }); }; async function yeet(input) { try { var thing = await doAThing(input); console.log(thing) } catch (err) { console.log(err); } } yeet("gottem"); yeet("OwO"); The function yeet is declared with an additional async keyword, telling the interpreter that the function is asynchronous. Next, the variable thing is assigned to the resolve value of doAThing after it completes execution. This is important, as the rest of the function will not execute until doAThing either resolves or rejects. When doAThing receives "OwO", it will reject, causing the try/catch statement in yeet to fail. The error is then logged. This will output: gottem!!1111!! No OwOs in the chat

Conclusion

Asynchronous code is highly useful in JavaScript, as it allows for concurrent code snippets to run. Without it, most websites would not work in the way they are supposed to. We explored the three main ways to write asynchronous code in JavaScript, being callbacks, promises, and async/await.

Thank you for reading.

-Valphalia Hoopes