Async vs sync code

By

Understand the difference between synchronous and asynchronous code, and why async APIs let Node.js handle more traffic than blocking languages like PHP.

~~~

Synchronous code runs one operation at a time: each line blocks the program until it’s done. Asynchronous code starts an operation, lets the rest of the program keep running, and gets called back when the result is ready.

You might have heard that Node.js is fast because it provides asynchronous APIs for all expensive operations, like network access or filesystem.

What does having an asynchronous API mean?

If you anticipate an operation can take a lot of time, it makes sense to run it asynchronously, so other code can run in the meantime, and have a hook that’s called when that operation ends.

Let’s see the difference in code

Node.js offers both versions for reading a file, in its built-in fs module. The node: prefix in require('node:fs') tells Node, and anyone reading the code, that fs is the built-in module and not a package from npm.

This is the synchronous version:

const fs = require('node:fs')

const data = fs.readFileSync('/Users/flavio/notes.txt', 'utf8')
console.log(data)
console.log('done')

Nothing else runs while the file is being read. First the file content prints, then done.

This is the asynchronous version:

const fs = require('node:fs')

fs.readFile('/Users/flavio/notes.txt', 'utf8', (err, data) => {
  if (err) {
    console.error(err)
    return
  }
  console.log(data)
})
console.log('done')

Here done prints first. The program didn’t stop and wait for the disk. It registered a callback, moved on, and the callback ran later, when the file data was ready.

The promise version

Modern Node code usually skips the callback. Almost every method of the fs module also has a promise version in node:fs/promises, so you can use await:

import { readFile } from 'node:fs/promises'

const data = await readFile('/Users/flavio/notes.txt', 'utf8')
console.log(data)
console.log('done')

It reads top to bottom like the sync version, but it’s still asynchronous. await pauses only this piece of code while the disk works. In the meantime Node keeps running timers and serving other requests.

done prints after the file content, because it comes after the await. Top-level await works in ES modules, so save this file with the .mjs extension.

Why does this matter so much?

Node.js runs your JavaScript on a single thread. If that thread is blocked waiting for a file or a network response, nothing else happens: no other request is served, no timer fires.

This is how Node.js can handle a lot more traffic than, say, PHP or Rails without using async libraries.

What usually happens in PHP or Python code is that the thread blocks until the sync operation (reading from the network, writing a file..) ends. The server compensates by running many threads or processes, one per request, which costs memory.

If the code runs asynchronously, the CPU is not idle waiting for the process to complete. It can go on with other tasks queued up until the original operation is ready to move on.

Most programming languages that were not traditionally async today do have 3rd party libraries that implement ways to call asynchronous code.

A common pitfall

The sync versions look tempting because the code reads top to bottom. But be careful where you use them.

Calling fs.readFileSync() inside an HTTP server request handler blocks the entire server for the duration of the read. Every other client waits. With one slow file and some traffic, response times fall apart.

In a server, always use the asynchronous versions. Sync APIs are fine in command line scripts, where there’s nobody else waiting.

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about js: