Async JavaScript: history, patterns and gotchas
Tags: javascript, async, patterns, node
Published: 2019-01-28 08:11:07

---
title: Async JavaScript: history, patterns and gotchas
published: true
tags: JavaScript, Async, patterns, node
canonical_url: https://codewithhugo.com/async-js/
---

A look at the history, patterns and gotchas of asynchronous operations in JavaScript.

We’ll go through the pros and cons of callbacks, Promises and async/await. Present some pitfalls to bear in mind as well as introducing how you would deal with certain situations.

Live-coding/workshop section touching on both Node and client-side JS situations at [github.com/HugoDF/async-js-presentation/tree/master/workshop](https://github.com/HugoDF/async-js-presentation/tree/master/workshop).

This given as a talk at Codebar London January Monthly 2019, see the slides:


[View the original slides on SpeakerDeck](https://speakerdeck.com/hugodf/async-javascript-history-patterns-and-gotchas) or [from the GitHub repo](https://github.com/HugoDF/async-js-presentation).


**Table of Contents 🐳** :

- [Asynchronicity in JavaScript](#asynchronicity-in-javascript)
  - [What's asynchronous in a web application?](#whats-asynchronous-in-a-web-application)
  - [Why non-blocking I/O?](#why-non-blocking-io)
- [Node-style callbacks](#node-style-callbacks)
  - [Node-style callbacks: problems](#node-style-callbacks-problems)
    - [1. Callback *hell*](#1-callback-hell)
    - [2. Shadowing variables](#2-shadowing-variables)
    - [3. Duplicated error handling](#3-duplicated-error-handling)
    - [4. Swallowed errors](#4-swallowed-errors)
  - [Callback problems](#callback-problems)
- [Bring on the Promise](#bring-on-the-promise)
  - [Pros](#pros)
  - [Cons](#cons)
  - [Promise gotchas](#promise-gotchas)
    - [Nesting them is tempting](#nesting-them-is-tempting)
  - [onRejected callback](#onrejected-callback)
- [async/await](#asyncawait)
  - [Example: loop through sequential calls](#example-loop-through-sequential-calls)
  - [Example: share data between calls](#example-share-data-between-calls)
  - [Example: error handling](#example-error-handling)
  - [Cons of async/await](#cons-of-asyncawait)
- [Gotchas](#gotchas)
  - [Creating an error](#creating-an-error)
  - [What happens when you forget await?](#what-happens-when-you-forget-await)
  - [Promises evaluate eagerly ✨](#promises-evaluate-eagerly-)
  - [Testing gotchas 📙](#testing-gotchas-)
- [Patterns](#patterns)
  - [Running promises in parallel 🏃](#running-promises-in-parallel-)
  - [Delay execution of a promise](#delay-execution-of-a-promise)
  - [Separate synchronous and asynchronous operations](#separate-synchronous-and-asynchronous-operations)
  - [Running promises sequentially](#running-promises-sequentially)
  - [Passing data in sequential async calls](#passing-data-in-sequential-async-calls)
  - [Error handling](#error-handling)
- [Workshop Examples](#workshop-examples)
  - ["callbackify"-ing a Promise-based API](#callbackify-ing-a-promise-based-api)
  - [Getting data in parallel using callbacks: the pain](#getting-data-in-parallel-using-callbacks-the-pain)
  - ["promisify"-ing a callback-based API](#promisify-ing-a-callback-based-api)
  - [Why we don't mix async and sync operations](#why-we-dont-mix-async-and-sync-operations)
- [Further Reading](#further-reading)

## Asynchronicity in JavaScript

Primitives:- Callbacks- Promises- (Observables)- `async/await`

### What’s asynchronous in a web application?

Most things:1. any network calls (HTTP, database)2. timers (`setTimeout`, `setInterval`)3. filesystem access… Anything else that can be offloaded

In JavaScript, these operations are non-blocking.

HTTP Request in Python:

```
data = request(myUrl)
print(data)
```

HTTP Request in JavaScript:

```
request(myUrl, (err, data) => {
  console.log(data);
});

```

### Why non-blocking I/O?

JavaScript was conceived as a UI programming language. In UI, you don’t want to freeze UI interactions while you wait for a server to respond for example.

Non-blocking I/O means waiting doesn’t cost you compute cycles.

How non-blocking I/O is implemented (in JavaScript):- pass a “callback” function- it’s called with the outcome of the async operation

## Node-style callbacks

```
myAsyncFn((err, data) => {
  if (err) dealWithIt(err);
  doSomethingWith(data);
})

```

A callback is:

- “just” a function
- in examples, usually anonymous functions (pass `function () {}` directly)
- according to some style guides, should be an arrow function (`() => {}`)
- called when the async operation

A Node-style callback is:

- called with any error(s) as the first argument/parameter, if there’s no error, `null` is passed
- called with any number of “output” data as the other arguments

ie. `(err, data) => { /* more logic */ }`

### Node-style callbacks: problems

#### 1. Callback _hell_

```
myAsyncFn((err, data) => {
  if (err) handle(err)
  myOtherAsyncFn(data, (err, secondData) => {
    fun(data, secondData, (err) => {
      if (err) handle(err)
    })
    fn(data, secondData, (err) => {
      if (err) handle(err)
    })
  })
})

```

For each asynchronous operation:- extra level of indent- lots of names for async output: `data`, `secondData`

#### 2. Shadowing variables

```
myAsyncFn((err, data) => {
  if (err) handle(err)
  myOtherAsyncFn(data, (err, secondData) => {
    fun(data, secondData, (err) => {
      if (err) handle(err)
    })
    fn(data, secondData, (err) => {
      if (err) handle(err)
    })
  })
})

```

- `err` (in `myAsyncFn` callback) !== `err` (in `myOtherAsyncFn` callback) despite having the same nam

#### 3. Duplicated error handling

- 1 call to `handle(err)` per operation

```
myAsyncFn((err, data) => {
  if (err) handle(err)
  myOtherAsyncFn(data, (err, secondData) => {
    fun(data, secondData, (err) => {
      if (err) handle(err)
    })
    fn(data, secondData, (err) => {
      if (err) handle(err)
    })
  })
})

```

#### 4. Swallowed errors

Ideal failure:- fail early- fail fast- fail loud

Spot the unhandled error:

```
myAsyncFn((err, data) => {
  if (err) handle(err)
  myOtherAsyncFn(data, (err, secondData) => {
    fun(data, secondData, (err) => {
      if (err) handle(err)
    })
    fn(data, secondData, (err) => {
      if (err) handle(err)
    })
  })
})

```

The **silent error** is where the comment is.

```
myAsyncFn((err, data) => {
  if (err) handle(err)
  myOtherAsyncFn(data, (err, secondData) => {
    // Missing error handling!
    fun(data, secondData, (err) => {
      if (err) handle(err)
    })
    fn(data, secondData, (err) => {
      if (err) handle(err)
    })
  })
})

```

That `err` doesn’t get handled. Linters would have caught that (I hope), whining that `err` was defined but not used. That’s living on the edge a little bit.

### Callback problems

The issues with callbacks boil down to the following.

**Callback hell** with its many indents and variable names.

**Shadowed variables** with all the issues that brings.

**Duplicated error-handling** which makes it easy to **swallow errors**.

## Bring on the Promise

```
myAsyncFn()
  .then((data) => Promise.all([
    data,
    myOtherAsyncFn(data),
  ]))
  .then(([data, secondData]) => Promise.all([
    fun(data, secondData),
    fn(data, secondData),
  ]))
  .then(/* do anything else */)
  .catch((err) => handle(err));

```

### Pros

Promises are **chainable** , you can return a Promise from `.then`, tack another `.then` and keep it going, no crazy indent stuff.

You can define a **single error handler** using `.catch` added to the end of your promise chain.

One small function per async step (inside `.then`) makes it easier to break down long asynchronous flows.

### Cons

You define a lot of tightly scoped functions, passing data from one call to the next is **very verbose** eg.:

```
.then((data) => Promise.all([
  data,
  myOtherAsyncFn(data),
])

```

### Promise gotchas

#### Nesting them is tempting

```
myAsyncFn()
  .then((data) =>
    myOtherAsyncFn(data)
      .then(
        ([data, secondData]) =>
          Promise.all([
            fun(data, secondData),
            fn(data, secondData),
          ])
      )
  )
  .catch((err) => handle(err))

```

Solution: Avoid the Pyramid of Doom ☠️

```
myAsyncFn()
  .then((data) => Promise.all([
    data,
    myOtherAsyncFn(data),
  ]))
  .then(([data, secondData]) => Promise.all([
    fun(data, secondData),
    fn(data, secondData),
  ]))
  .then(/* do anything else */)
  .catch((err) => handle(err))

```

Promises “flatten”, you can return a Promise from a `then` and keep adding `.then` that expects the resolved value.

### onRejected callback

`.then` takes two parameters, `onResolved` and `onRejected`, so the following works:

```
myAsyncFn()
  .then(
    (data) => myOtherAsyncFn(data),
    (err) => handle(err)
  );

```

But we’re back to doing per-operation error-handling like in callbacks (potentially swallowing errors etc.)

Solution: avoid it, in favour of `.catch`

```
myAsyncFn()
  .then(
    (data) => myOtherAsyncFn(data)
  )
  .catch((err) => handle(err));

```

_Unless you specifically need it_, eg. when you use `redux-thunk` and making HTTP calls, you also `.catch` rendering errors from React.

In that case, it’s preferrable to use `onRejected`.

## async/await

```
(async () => {
  try {
    const data = await myAsyncFn();
    const secondData = await myOtherAsyncFn(data);
    const final = await Promise.all([
      fun(data, secondData),
      fn(data, secondData),
    ]);
    /* do anything else */
  } catch (err) {
    handle(err);
  }
})();

```

Given a Promise (or any object that has a `.then` function), `await` takes the value passed to the callback in `.then`.

`await` can only be used inside a function that is `async`.Top-level (outside of async function) await is coming, currently you’ll get a syntax error though.

```
(async () => {
  console.log('Immediately invoked function expressions (IIFEs) are cool again')
  const res = await fetch('https://jsonplaceholder.typicode.com/todos/2')
  const data = await res.json()
  console.log(data)
})()

// SyntaxError: await is only valid in async function
const res = await fetch(
  'https://jsonplaceholder.typicode.com/todos/2'
)

```

`async` functions are “just” Promises. Which means you can call an `async` function and tack a `.then` onto it.

```
const arrow = async () => { return 1 }
const implicitReturnArrow = async () => 1
const anonymous = async function () { return 1 }
async function expression () { return 1 }

console.log(arrow()); // Promise { 1 }
console.log(implicitReturnArrow()); // Promise { 1 }
console.log(anonymous()); // Promise { 1 }
console.log(expression()); // Promise { 1 }

```

### Example: loop through sequential calls

With async/await:

```
async function fetchSequentially(urls) {
  for (const url of urls) {
    const res = await fetch(url);
    const text = await res.text();
    console.log(text.slice(0, 100));
  }
}

```

With promises:

```
function fetchSequentially(urls) {
  const [url, ...rest] = urls
  fetch(url)
    .then(res => res.text())
    .then(text => console.log(text.slice(0, 100)))
    .then(fetchSequentially(rest));
}

```

### Example: share data between calls

`const myVariable = await fetchThing()` -> easy

```
async function run() {
  const data = await myAsyncFn();
  const secondData = await myOtherAsyncFn(data);
  const final = await Promise.all([
    fun(data, secondData),
    fn(data, secondData),
  ]);

  return final
}

```

We don’t have the whole Promise-flow of:

```
.then(() => Promise.all([dataToPass, promiseThing]))
.then(([data, promiseOutput]) => { })

```

### Example: error handling

In the following example, the `try/catch` gets any error and logs it.

The caller of the function has no idea anything failed.

```
async function withErrorHandling(url) {
  try {
    const res = await fetch(url);
    const data = await res.json();
    return data
  } catch(e) {
    console.log(e.stack)
  }
}

withErrorHandling(
  'https://jsonplaceholer.typicode.com/todos/2'
  // The domain should be jsonplaceholder.typicode.com
).then(() => { /* but we'll end up here */ })

```

### Cons of async/await

Browser support is only good in latest/modern browsers.

Polyfills (async-to-gen, regenerator runtime) are big, so sticking to Promises if you’re only using async/await for syntactic sugar is a good idea.

Node 8+ supports it natively though, no plugins, no transpilation, no polyfills, so async/await away there.

Keen functional programming people would say it leads to a more “imperative” style of programming, I don’t like indents so I don’t listen to that argument.

## Gotchas

### Creating an error

`throw`-ing inside an `async` function and `return Promise.reject` work the same

`.reject` and `throw` `Error` objects please, you never know which library might do an `instanceof Error` check.

```
async function asyncThrow() {
  throw new Error('asyncThrow');
}
function rejects() {
  return Promise.reject(new Error('rejects'))
}
async function swallowError(fn) {
  try { await asyncThrow() }
  catch (e) { console.log(e.message, e. __proto__ ) }
  try { await rejects() }
  catch (e) { console.log(e.message, e. __proto__ ) }
}
swallowError() // asyncThrow Error {} rejects Error {}

```

### What happens when you forget await?

Values are undefined, Promise is an object that has few properties.

You’ll often see: `TypeError: x.fn is not a function`.

```
async function forgotToWait() {
  try {
    const res = fetch('https://jsonplaceholer.typicode.com/todos/2')
    const text = res.text()
  } catch (e) {
    console.log(e);
  }
}

forgotToWait()
// TypeError: res.text is not a function

```

The `console.log` output of Promise/async function (which is just a Promise) is: `Promise { <pending> }`.

When you start debugging your application and a variable that was supposed to contain a value logs like that, you probably forgot an `await` somewhere.

```
async function forgotToWait() {
  const res = fetch('https://jsonplaceholer.typicode.com/todos/2')
  console.log(res)
}

forgotToWait()
// Promise { <pending> }

```

### Promises evaluate eagerly ✨

Promises don’t wait for anything to execute, when you create it, it runs:

```
new Promise((resolve, reject) => {
  console.log('eeeeager');
  resolve();
})

```

The above code will immediately print ‘eeeeager’, tip: don’t create Promises you don’t want to run.

### Testing gotchas 📙

Jest supports Promises as test output (therefore also `async` functions):

```
const runCodeUnderTest = async () => {
  throw new Error();
};

test('it should pass', async () => {
  doSomeSetup();

  await runCodeUnderTest();
  // the following never gets run
  doSomeCleanup();
})

```

If you test fails, the `doSomeCleanup` function doesn’t get called so you might get cascading failures.

Do your cleanup in “before/after” hooks, async test bodies crash and don’t clean up.

```
describe('feature', () => {
  beforeEach(() => doSomeSetup())
  afterEach(() => doSomeCleanup())
  test('it should pass', async () => {
    await runCodeUnderTest();
  })
})

```

## Patterns

A lot of these are to avoid the pitfalls we’ve looked in the “gotchas” section.

### Running promises in parallel 🏃

Using `Promise.all`, which expects an array of Promises, waits until they all resolve (complete) and calls `.then` handler with the array of resolved values.

```
function fetchParallel(urls) {
  return Promise.all(
    urls.map(
      (url) =>
      fetch(url).then(res => res.json())
    )
  );
}

```

Using `Promise.all` + `map` over an `async` function, an async function is… “just a Promise”.

Good for logging or when you’ve got non-trivial/business logic

```
function fetchParallel(urls) {
  return Promise.all(
    urls.map(async (url) => {
      const res = await fetch(url);
      const data = await res.json();
      return data;
    })
  );
}

```

### Delay execution of a promise

Promises are eager, they just wanna run! To delay them, wrap them in a function that returns the Promise.

```
function getX(url) {
  return fetch(url)
}

// or

const delay = url => fetch(url)

```

No Promise, no eager execution. Fancy people would call the above “thunk”, which is a pattern to delay execution/calculation.

### Separate synchronous and asynchronous operations

A flow in a lot of web applications that rely on asynchronous operations for read and write is the following.

Fetch data, doing an asynchronous operation. Run synchronous operations using the data in-memory. Write the data back with an asynchronous call.

```
const fs = require('fs').promises

const fetchFile = () =>
  fs.readFile('path', 'utf-8');
const replaceAllThings = (text) =>
  text.replace(/a/g, 'b');
const writeFile = (text) =>
  fs.writeFile('path', text, 'utf-8');

(async () => {
  const text = await fetchFile();
  const newText = replaceAllThings(text);
  await writeFile(newText);
})();

```

A lot of built-in functions don’t wait for a Promise to resolve. If you mix string manipulation/replacement and Promises you’ll end up with `[object Promise]` everywhere your code injected the Promise object instead of the resolved value.

### Running promises sequentially

Using recursion + rest/spread and way too much bookkeeping…

```
function fetchSequentially(urls, data = []) {
  if (urls.length === 0) return data
  const [url, ...rest] = urls
  return fetch(url)
    .then(res => res.text())
    .then(text =>
      fetchSequentially(
        rest,
        [...data, text]
      ));
}

```

Using `await` + a loop, less bookkeeping, easier to read.

```
async function fetchSequentially(urls) {
  const data = []
  for (const url of urls) {
    const res = await fetch(url);
    const text = await res.text();
    data.push(text)
  }
  return data
}

```

Remember to only make sequeuntial calls if the nth call rely on a previous call’s output. Otherwise you might be able to run the whole thing in parallel.

### Passing data in sequential async calls

Return array + destructuring in next call, very verbose in Promise chains:

```
async function findLinks() { /* some implementation */ }

function crawl(url, parentText) {
  console.log('crawling links in: ', parentText);
  return fetch(url)
    .then(res => res.text())
    .then(text => Promise.all([
      findLinks(text),
      text
    ]))
    .then(([links, text]) => Promise.all(
      links.map(link => crawl(link, text))
    ));
}

```

Using `await` + data in the closure:

```
async function findLinks() { /* someimplementation */ }

async function crawl(url, parentText) {
  console.log('crawling links in: ', parentText);
  const res = await fetch(url);
  const text = await res.text();
  const links = await findLinks(text);
  return crawl(links, text);
}

```

### Error handling

Using try/catch, or `.catch`, try/catch means you’ll also be `catch`-ing synchronous errors.

```
function withCatch() {
  return fetch('borked_url')
    .then(res => res.text())
    .catch(err => console.log(err))
}

async function withBlock() {
  try {
    const res = await fetch('borked_url');
    const text = await res.text();
  } catch (err) {
    console.log(err)
  }
}

```

## Workshop Examples

Example code at [github.com/HugoDF/async-js-presentation/tree/master/workshop](https://github.com/HugoDF/async-js-presentation/tree/master/workshop)

### “callbackify”-ing a Promise-based API

We’re going to take `fetch` (see [MDN article on fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)),a browser API that exposes a Promise-based API to make HTTP calls.

We’re going to write a `get(url, callback)` function, which takes a URL, fetches JSON from it and calls the callback with it (or with the error).

We’ll use it like this:

```
get('https://jsonplaceholder.typicode.com/todos', (err, data) => {
  console.log(data)
})

```

To being with let’s define a `get` function with the right parameters, call fetch for the URL and get data:

```
// only needed in Node
const fetch = require('node-fetch')

function get(url, callback) {
  fetch(url)
    .then((res) => res.json())
    .then((data) => { /* we have the data now */})
}

```

Once we have the data, we can call `callback` with `null, data`:

```
// only needed in Node
const fetch = require('node-fetch')

function get(url, callback) {
  fetch(url)
    .then((res) => res.json())
    .then((data) => callback(null, data))
}

```

And add the error handling step, `.catch((err) => callback(err))`:

```
// only needed in Node
const fetch = require('node-fetch')

function get(url, callback) {
  fetch(url)
    .then((res) => res.json())
    .then((data) => callback(null, data))
    .catch((err) => callback(err))
}

```

That’s it, we’ve written a wrapper that uses a callback API to make HTTP requests with a Promise-based client.

### Getting data in parallel using callbacks: the pain

Next we’ll write a function that gets todos by id from the jsonplaceholder API using the `get` function we’ve defined in the previous section.

Its usage will look something like this (to get ids 1, 2, 3, 10, 22):

```
getTodosCallback([1, 2, 3, 10, 22], (err, data) => {
  if (err) return console.log(err)
  console.log(data)
})

```

Let’s define the function, we take the array of ids, and call `get` with its URL (baseUrl + id).

In the callback to the `get`, we’ll check for errors.

Also, if the data for all the ids has been fetched, we’ll call the callback with all the data.

That’s a lot of bookkeeping and it doesn’t even necessarily return the data in the right order.

```
const baseUrl = 'https://jsonplaceholder.typicode.com/todos'

function getTodosCallback(ids, callback) {
  const output = []
  const expectedLength = ids.length

  ids.forEach(id => {
    get(`${baseUrl}/${id}`, (err, data) => {
      if (err) callback(err)

      output.push(data)

      if (output.length === expectedLength) {
        callback(null, output)
      }
    })
  })
}

```

Here’s the same functionality implemented with straight `fetch`:

```
function getTodosPromise(ids) {
  return Promise.all(
    ids.map(async (id) => {
      const res = await fetch(`${baseUrl}/${id}`);
      const data = await res.json();
      return data;
    })
  )
}

```

Shorter, denser, and returns stuff in order.

### “promisify”-ing a callback-based API

Historically Node’s APIs and `fs` in particular have used a callback API.

Let’s read a file using a Promise instead of `readFile(filePath, options, (err, data) => {})`.

We want to be able to use it like so:

```
readFile('./01-callbackify-fetch.js', 'utf8')
  .then(console.log)

```

The `Promise` constructor takes a function which has 2 arguments, resolve and reject. They’re both functions and we’ll want to `resolve()` with a sucessful value and `reject()` on error.

So we end up with the following:

```
const fs = require('fs')

function readFile(path, encoding) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, encoding, (err, text) => {
      if (err) return reject(err)
      resolve(text)
    })
  })
}

```

That’s all there is to it.

### Why we don’t mix async and sync operations

Let’s define an abritrary problem: I have some JSON files with information about browsers in a folder.

Given a piece of text that contains the browser name I would like to inject the statistics from the files in the folder.

Let’s do a naive implementation, we have a `loadBrowserData` async function that reads the file and `JSON.parse`-s it.

We have a `badIdea` async function which loops through browsers and calls `text.replace()` with the browser name as the first parameter and an async function that fetches data and formats it as the second.

`String.replace` does support a callback as the second parameter but it doesn’t `await` it, it just expects a synchronous function, which means the following code:

```
const fs = require('fs').promises
const path = require('path')

const browsers = ['chrome', 'edge', 'firefox', 'safari']

async function loadBrowserData(name) {
  const data = await fs.readFile(path.resolve(__dirname, './04-data', `${name}.json`), 'utf8');
  return JSON.parse(data)
}

async function badIdea(text) {
  let newText = text
  browsers.forEach((browser) => {
    newText = newText.replace(browser, async (match) => {
      const {
        builtBy,
        latestVersion,
        lastYearUsage
      } = await loadBrowserData(browser);
      return `${browser} (${builtBy}, latest version: ${latestVersion}, usage: ${lastYearUsage})`
    })
  })
  return newText
}

const myText = `
We love chrome and firefox.

Despite their low usage, we also <3 safari and edge.
`;

(async () => {
  console.log(await badIdea(myText));
})()

```

Logs out:

```
We love [object Promise] and [object Promise].

Despite their low usage, we also <3 [object Promise] and [object Promise].
```

If instead we load up all the browser data beforehand and use it synchronously, it works:

```
const fs = require('fs').promises
const path = require('path')

const browsers = ['chrome', 'edge', 'firefox', 'safari']

async function loadBrowserData(name) {
  const data = await fs.readFile(path.resolve(__dirname, './04-data', `${name}.json`), 'utf8');
  return JSON.parse(data)
}

async function betterIdea(text) {
  const browserNameDataPairs = await Promise.all(
    browsers.map(
      async (browser) => [browser, await loadBrowserData(browser)]
    )
  );
  const browserToData = browserNameDataPairs.reduce((acc, [name, data]) => {
    acc[name] = data
    return acc
  }, {})

  let newText = text

  browsers.forEach((browser) => {
    newText = newText.replace(browser, () => {
      const {
        builtBy,
        latestVersion,
        lastYearUsage
      } = browserToData[browser];
      return `${browser} (${builtBy}, latest version: ${latestVersion}, usage: ${lastYearUsage})`
    })
  })

  return newText
}

const myText = `
We love chrome and firefox.

Despite their low usage, we also <3 safari and edge.
`;

(async () => {
  console.log(await betterIdea(myText));
})()

```

It logs out the expected:

```
We love chrome (Google, latest version: 71, usage: 64.15%) and firefox (Mozilla, latest version: 64, usage: 9.89%).

Despite their low usage, we also <3 safari (Apple, latest version: 12, usage: 3.80%) and edge (Microsoft, latest version: 18, usage: 4.30%).

```

## Further Reading

- About non-blocking I/O in Node.js docs: [nodejs.org/en/docs/guides/blocking-vs-non-blocking/](https://nodejs.org/en/docs/guides/blocking-vs-non-blocking/)
- [Async JavaScript: From Callbacks, to Promises, to Async/Await](https://tylermcginnis.com/async-javascript-from-callbacks-to-promises-to-async-await/) by Tyler McGinnis

Are good reads in and around this subject. The secret to understanding asynchronous JavaScript behaviour is to experiment: turn callbacks into Promises and vice-versa.

[View the original slides on SpeakerDeck](https://speakerdeck.com/hugodf/async-javascript-history-patterns-and-gotchas) or [from the GitHub repo](https://github.com/HugoDF/async-js-presentation).

Let me know [@hugo\_\_df](https://twitter.com/hugo__df) if you need a hand 🙂.