strace is your last resort when a Node.js app is misbehaving in ways that aren’t obvious from its logs or application-level debugging. It’s the tool that shows you exactly what your program is asking the operating system to do, and how the OS is responding.

Imagine your Node.js application is a chef in a kitchen. strace is like a silent observer who meticulously records every single request the chef makes to the kitchen staff: "Chef, I need 2 cups of flour!" (a read syscall), "Chef, put this cake in the oven!" (a write syscall), "Chef, where’s the oven knob?" (a stat syscall). It shows you the exact words and the exact outcomes.

Let’s see strace in action. Suppose you have a tiny Node.js script that just tries to read a file that doesn’t exist:

const fs = require('fs');

try {
  fs.readFileSync('non-existent-file.txt');
} catch (err) {
  console.error('Caught error:', err.message);
}

If you run this with node your_script.js, you’ll see Caught error: ENOENT: no such file or directory, open 'non-existent-file.txt'. That’s the application-level view. Now, let’s strace it:

strace -f -e trace=open,read,write,close node your_script.js

Here’s a snippet of what you might see:

...
openat(AT_FDCWD, "non-existent-file.txt", O_RDONLY) = -1 ENOENT (No such file or directory)
read(3,  <unfinished ...>
write(1, "Caught error: ENOENT: no such file or directory, open 'non-existent-file.txt'\n", 86) = 86
close(1)                                = 0
...

The -f flag tells strace to follow child processes (important for Node.js worker threads or child processes spawned by your app). The -e trace=open,read,write,close filters the output to show only the syscalls we’re interested in for this example.

The key line is openat(AT_FDCWD, "non-existent-file.txt", O_RDONLY) = -1 ENOENT (No such file or directory). This is the direct interaction with the OS. openat is the syscall Node.js uses to open files. AT_FDCWD means the path is relative to the current working directory. "non-existent-file.txt" is the filename. O_RDONLY is the flag indicating it’s for reading. And critically, = -1 ENOENT means the system call failed, returning an error code ENOENT (which stands for "Error NO ENTry" or "Error NO ENTity"). This is precisely what your Node.js code then catches as an exception.

strace is incredibly powerful for understanding performance bottlenecks. If your app is slow, strace can reveal excessive I/O operations, frequent stat calls on the same files, or unexpected network activity. It’s also invaluable for debugging issues like file descriptor leaks, permission problems, or how your application interacts with shared memory or inter-process communication mechanisms.

The mental model to build is that your Node.js code, running in the V8 JavaScript engine, is a layer of abstraction over the operating system. V8 and Node.js’s C++ bindings translate your JavaScript calls (like fs.readFileSync) into specific system calls that the Linux kernel (or macOS, or Windows kernel) understands. strace lets you peer between these layers, seeing the raw requests and responses.

When debugging, you’ll often see a pattern of openat, read, write, and close for file operations. For network operations, you’ll see syscalls like socket, connect, sendto, recvfrom, and poll (or epoll_wait on Linux). Understanding the arguments to these syscalls is key: what file descriptor is being used, what data is being sent, what address is being connected to, and what flags are being set.

A common pitfall is not realizing that Node.js might be making multiple syscalls for what appears to be a single operation in JavaScript. For example, reading a large file might involve a single read syscall with a large buffer, or it might involve several smaller read calls depending on how the fs module internally buffers data and how the OS handles large reads. strace will show you the granular reality.

If you’re seeing your Node.js process hanging or consuming excessive CPU, strace is your best friend. You can attach strace to a running process using strace -p <PID>. Look for syscalls that are taking a long time to return, or for repeated syscalls that seem unnecessary. For instance, if you see a select or poll call that seems to be waiting indefinitely, it means your application is blocked waiting for an event that might never come, or is coming much later than expected.

The most surprising thing about strace for many Node.js developers is the sheer volume of low-level interaction that even simple JavaScript code triggers. It’s not just the fs module; even basic operations like printing to stdout involve write syscalls, and module loading involves openat and read calls for .js files and their dependencies.

When you’re deep in strace output and see a syscall like futex, it’s often an indicator of low-level synchronization primitives being used, which are typically managed by the Node.js runtime itself for things like asynchronous I/O completion or thread synchronization. If you see futex calls that seem to be spinning or blocking unexpectedly, it might point to an internal V8 or Node.js runtime issue, or a complex interaction between asynchronous operations that’s leading to contention.

The next step after mastering strace for syscall tracing is often to dive into kernel-level tracing with bpftrace or perf, which offer more advanced profiling and tracing capabilities without the overhead of intercepting every syscall.

Want structured learning?

Take the full Strace course →