When a call into the operating system fails — opening a file, binding a port, resolving a name — libuv hands back a system error: an ordinary Error with extra properties. code is the stable string identifier, errno the negative platform number, syscall the failed operation, plus whichever of path, address, port and hostname applies.
import { readFile } from 'node:fs/promises';
import { getSystemErrorName, getSystemErrorMessage } from 'node:util';
try {
await readFile('./config/app.json');
} catch (err) {
console.log(err.code, err.errno, err.syscall, err.path);
console.log(getSystemErrorName(err.errno), '-', getSystemErrorMessage(err.errno));
}ENOENT -4058 open C:\dev\shop\config\app.json ENOENT - no such file or directory
util.getSystemErrorName() and util.getSystemErrorMessage() (both stable) turn a raw errno back into something readable. Note that errno above: on Unix libuv uses the negated C errno, so the same missing file reports -2 on Linux and macOS, while Windows uses arbitrary libuv numbers. Branch on code, never on errno, and never on message, which the Node documentation reserves the right to reword between releases.
| Code | Source | Usual meaning |
|---|---|---|
| ENOENT | fs, child_process | Path or binary not found |
| EACCES / EPERM | fs, net | No permission; port below 1024 |
| EADDRINUSE / ECONNREFUSED | net, http | Port held; nothing listening |
| ECONNRESET / EPIPE | sockets | Peer closed mid-transfer |
| ENOTFOUND | dns, fetch | Hostname did not resolve |
Node's own JavaScript-level errors use the same field with a different naming scheme: uppercase and prefixed with ERR_ — ERR_INVALID_ARG_TYPE, ERR_MODULE_NOT_FOUND, ERR_UNHANDLED_REJECTION. Treat both families as one vocabulary and turn the common ones into advice: answering EADDRINUSE with ` console.error(Port ${err.port} is busy) and process.exit(1) costs three lines and saves the next developer a stack trace from node:net`.