JavaScript Asynchronous Programming and Callbacks

Asynchronicity in Programming Languages

Computers are asynchronous by design.

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

In the current consumer computers, every program runs for a specific time slot and then it stops its execution to let another program continue their execution. This thing runs in a cycle so fast that it's impossible to notice. We think our computers run many programs simultaneously, but this is an illusion (except on multiprocessor machines).

Programs internally use interrupts, a signal that's emitted to the processor to gain the attention of the system.

Let's not go into the internals of this now, but just keep in mind that it's normal for programs to be asynchronous and halt their execution until they need attention, allowing the computer to execute other things in the meantime. When a program is waiting for a response from the network, it cannot halt the processor until the request finishes.

Normally, programming languages are synchronous and some provide a way to manage asynchronicity in the language or through libraries. C, Java, C#, PHP, Go, Ruby, Swift, and Python are all synchronous by default. Some of them handle async operations by using threads, spawning a new process.

JavaScript

JavaScript is synchronous by default and is single threaded. This means that code cannot create new threads and run in parallel.

Lines of code are executed in series, one after another, for example:

const const a: 1a = 1;
const const b: 2b = 2;
const const c: numberc = const a: 1a * const b: 2b;
var console: Consoleconsole.Console.log(...data: any[]): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(const c: numberc);
doSomething();

But 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?

The answer was in its environment. The browser provides a way to do it by providing a set of APIs that can handle this kind of functionality.

More recently, Node.js introduced a non-blocking I/O environment to extend this concept to file access, network calls and so on.

Callbacks

You can't know when a user is going to click a button. So, you define an event handler for the click event. This event handler accepts a function, which will be called when the event is triggered:

var document: Document
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/document)
document
.Document.getElementById(elementId: string): HTMLElement | null
Returns a reference to the first object with the specified value of the ID attribute.
@paramelementId String that specifies the ID value.
getElementById
('button').HTMLElement.addEventListener<"click">(type: "click", listener: (this: HTMLElement, ev: MouseEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)
Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched. The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options's capture. When set to true, options's capture prevents callback from being invoked when the event's eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event's eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event's eventPhase attribute value is AT_TARGET. When set to true, options's passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in § 2.8 Observing event listeners. When set to true, options's once indicates that the callback will only be invoked once after which the event listener will be removed. If an AbortSignal is passed for options's signal, then the event listener will be removed when signal is aborted. The event listener is appended to target's event listener list and is not appended if it has the same type, callback, and capture. [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)
addEventListener
('click', () => {
// item clicked });

This is the so-called callback.

A callback is a simple function that's passed as a value to another function, and will only be executed when the event happens. We can do this because JavaScript has first-class functions, which can be assigned to variables and passed around to other functions (called higher-order functions)

It's common to wrap all your client code in a load event listener on the window object, which runs the callback function only when the page is ready:

var window: Window & typeof globalThis
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/window)
window
.addEventListener<"load">(type: "load", listener: (this: Window, ev: Event) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)
Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched. The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options's capture. When set to true, options's capture prevents callback from being invoked when the event's eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event's eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event's eventPhase attribute value is AT_TARGET. When set to true, options's passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in § 2.8 Observing event listeners. When set to true, options's once indicates that the callback will only be invoked once after which the event listener will be removed. If an AbortSignal is passed for options's signal, then the event listener will be removed when signal is aborted. The event listener is appended to target's event listener list and is not appended if it has the same type, callback, and capture. [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)
addEventListener
('load', () => {
// window loaded // do what you want });

Callbacks are used everywhere, not just in DOM events.

One common example is by using timers:

function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout)
setTimeout
(() => {
// runs after 2 seconds }, 2000);

XHR requests also accept a callback, in this example by assigning a function to a property that will be called when a particular event occurs (in this case, the state of the request changes):

const 
module xhr
const xhr: XMLHttpRequest
xhr
= new var XMLHttpRequest: new () => XMLHttpRequest
Use XMLHttpRequest (XHR) objects to interact with servers. You can retrieve data from a URL without having to do a full page refresh. This enables a Web page to update just part of a page without disrupting what the user is doing. [MDN Reference](https://developer.mozilla.org/docs/Web/API/XMLHttpRequest)
XMLHttpRequest
();
module xhr
const xhr: XMLHttpRequest
xhr
.XMLHttpRequest.onreadystatechange: ((this: XMLHttpRequest, ev: Event) => any) | null
[MDN Reference](https://developer.mozilla.org/docs/Web/API/XMLHttpRequest/readystatechange_event)
onreadystatechange
= () => {
if (
module xhr
const xhr: XMLHttpRequest
xhr
.XMLHttpRequest.readyState: number
Returns client's state. [MDN Reference](https://developer.mozilla.org/docs/Web/API/XMLHttpRequest/readyState)
readyState
=== 4) {
if (
module xhr
const xhr: XMLHttpRequest
xhr
.XMLHttpRequest.status: number
[MDN Reference](https://developer.mozilla.org/docs/Web/API/XMLHttpRequest/status)
status
=== 200) {
var console: Consoleconsole.Console.log(...data: any[]): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(
module xhr
const xhr: XMLHttpRequest
xhr
.XMLHttpRequest.responseText: string
Returns response as text. Throws an "InvalidStateError" DOMException if responseType is not the empty string or "text". [MDN Reference](https://developer.mozilla.org/docs/Web/API/XMLHttpRequest/responseText)
responseText
);
} else { var console: Consoleconsole.Console.error(...data: any[]): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/error_static)
error
('error');
} } };
module xhr
const xhr: XMLHttpRequest
xhr
.XMLHttpRequest.open(method: string, url: string | URL): void (+1 overload)
Sets the request method, request URL, and synchronous flag. Throws a "SyntaxError" DOMException if either method is not a valid method or url cannot be parsed. Throws a "SecurityError" DOMException if method is a case-insensitive match for `CONNECT`, `TRACE`, or `TRACK`. Throws an "InvalidAccessError" DOMException if async is false, current global object is a Window object, and the timeout attribute is not zero or the responseType attribute is not the empty string. [MDN Reference](https://developer.mozilla.org/docs/Web/API/XMLHttpRequest/open)
open
('GET', 'https://yoursite.com');
module xhr
const xhr: XMLHttpRequest
xhr
.XMLHttpRequest.send(body?: Document | XMLHttpRequestBodyInit | null): void
Initiates the request. The body argument provides the request body, if any, and is ignored if the request method is GET or HEAD. Throws an "InvalidStateError" DOMException if either state is not opened or the send() flag is set. [MDN Reference](https://developer.mozilla.org/docs/Web/API/XMLHttpRequest/send)
send
();

Handling errors in callbacks

How do you handle errors with callbacks? One very common strategy is to use what Node.js adopted: the first parameter in any callback function is the error object: error-first callbacks

If there is no error, the object is null. If there is an error, it contains some description of the error and other information.

const import fsfs = require: anyrequire('node:fs');

import fsfs.readFile('/file.json', (err: anyerr, data: anydata) => {
  if (err: anyerr) {
    // handle error
    var console: Consoleconsole.Console.log(...data: any[]): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(err: anyerr);
return; } // no errors, process data var console: Consoleconsole.Console.log(...data: any[]): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(data: anydata);
});

The problem with callbacks

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 be complicated very quickly:

var window: Window & typeof globalThis
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/window)
window
.addEventListener<"load">(type: "load", listener: (this: Window, ev: Event) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)
Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched. The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options's capture. When set to true, options's capture prevents callback from being invoked when the event's eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event's eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event's eventPhase attribute value is AT_TARGET. When set to true, options's passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in § 2.8 Observing event listeners. When set to true, options's once indicates that the callback will only be invoked once after which the event listener will be removed. If an AbortSignal is passed for options's signal, then the event listener will be removed when signal is aborted. The event listener is appended to target's event listener list and is not appended if it has the same type, callback, and capture. [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)
addEventListener
('load', () => {
var document: Document
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/document)
document
.Document.getElementById(elementId: string): HTMLElement | null
Returns a reference to the first object with the specified value of the ID attribute.
@paramelementId String that specifies the ID value.
getElementById
('button').HTMLElement.addEventListener<"click">(type: "click", listener: (this: HTMLElement, ev: MouseEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)
Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched. The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options's capture. When set to true, options's capture prevents callback from being invoked when the event's eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event's eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event's eventPhase attribute value is AT_TARGET. When set to true, options's passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in § 2.8 Observing event listeners. When set to true, options's once indicates that the callback will only be invoked once after which the event listener will be removed. If an AbortSignal is passed for options's signal, then the event listener will be removed when signal is aborted. The event listener is appended to target's event listener list and is not appended if it has the same type, callback, and capture. [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)
addEventListener
('click', () => {
function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout)
setTimeout
(() => {
items.forEach(item: anyitem => { // your code here }); }, 2000); }); });

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

How do we solve this?

Alternatives to callbacks

Starting with ES6, JavaScript introduced several features that help us with asynchronous code that do not involve using callbacks: Promises (ES6) and Async/Await (ES2017).