Trust is earned, not given

A different perspective

2020-08-18 · Projects

Node.js, part 11: fs.promises and AbortController — doing two things at once, then stopping one

Part 11from the Node.js series · 24 parts in all

Two changes made cancellation a first-class idea in Node. The file system got a real promise API (fs.promises, later node:fs/promises), and the AbortController that came from the browser was adopted across core APIs. Part 11 is the pair, because they solve the same problem from two directions: overlapping work, and giving up on it.

fs.promises: the API you probably want

const fs = require('fs/promises');

// One import replaces the promisify boilerplate of part 2.
const entries = await fs.readdir('data', { withFileTypes: true });
const files = entries.filter((e) => e.isFile()).map((e) => e.name);

const stats = await Promise.all(files.map((f) => fs.stat(`data/${f}`)));

It is not a wrapper over the callback version — it is a separate implementation in libuv, marginally faster and with cleaner error objects. fs.rm with a recursive option also replaces the old rimraf dependency, which used to be a package almost every project installed.

AbortController: one signal, many APIs

const controller = new AbortController();
const { signal } = controller;

const p = fs.readFile('huge.log', { signal });
setTimeout(() => controller.abort(), 200);   // give up after 200ms

try {
  await p;
} catch (err) {
  if (err.name === 'AbortError') console.log('cancelled as intended');
  else throw err;
}

The value of a shared signal type is that one controller can cancel a whole operation: a file read, an fetch, a SQL query through a driver that supports it. That is what AbortSignal.timeout(ms) and AbortSignal.any([...]) compose into — a deadline plus a user-cancel, without hand-rolling the bookkeeping.

What cancellation is not

Aborting stops your code waiting; it cannot always stop the work underneath. A libuv read already in flight finishes and its buffer is discarded. The same is true of a remote query. So an abort is best understood as "detach and release the resources I hold", which is exactly what you want for a request whose client hung up — and it is why the cleanup path still has to run. Next: top-level await, which finally let a module be its own initialisation sequence.