JavaScript Basics: Distinguish a Symbolic Link from Its Target
When a path names a symbolic link, you can ask about the link or about the file it points to. A small temporary fixture shows why stat, lstat, and readlink answer different questions, including after the target disappears.
Prepare a scratch directory
Tested on Linux with Node.js v24.19.0. You need Node, Bash, a text editor, and the standard mktemp, rm, and rmdir utilities. No npm packages or remote services are needed. The .mjs extension selects ES modules.
Keep using the same terminal. Stop if setup fails, before saving the source:
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1Run the example
Save this as inspect-link.mjs:
import assert from 'node:assert/strict';
import { mkdtemp, writeFile, symlink, stat, lstat, readlink, unlink, rmdir } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const directory = await mkdtemp(join(tmpdir(), 'inspect-link-'));
const target = join(directory, 'report.txt');
const link = join(directory, 'latest');
let targetCreated = false;
let linkCreated = false;
try {
await writeFile(target, 'ready\n', { flag: 'wx' });
targetCreated = true;
await symlink('report.txt', link);
linkCreated = true;
assert.equal((await stat(link)).isFile(), true);
assert.equal((await lstat(link)).isSymbolicLink(), true);
assert.equal(await readlink(link), 'report.txt');
console.log('stat sees a regular file:', (await stat(link)).isFile());
console.log('lstat sees a symbolic link:', (await lstat(link)).isSymbolicLink());
console.log('Stored target:', await readlink(link));
await unlink(target);
targetCreated = false;
await assert.rejects(stat(link), { code: 'ENOENT' });
assert.equal((await lstat(link)).isSymbolicLink(), true);
console.log('Missing target: stat fails, lstat still finds the link');
} finally {
if (linkCreated) await unlink(link);
if (targetCreated) await unlink(target);
await rmdir(directory);
}Run it:
node inspect-link.mjsObserved output:
stat sees a regular file: true
lstat sees a symbolic link: true
Stored target: report.txt
Missing target: stat fails, lstat still finds the linkRead the result
stat follows the final symbolic link and describes its target. lstat describes that link itself. readlink returns the stored path text, which here is relative to the directory containing the link, not necessarily the process’s current directory. After removing report.txt, the link remains, but following it fails.
Use this distinction in file inventory and broken-link reports. It does not make a later open operation race-free: another process can change a path after inspection. Intermediate path components may still be followed by lstat. This fixture uses a private temporary directory and removes only the paths it created; it does not inspect or modify existing links.
The official Node.js reference describes the API. The assertions above check the specific fixture and its intended edge case.
Clean up
Remove the source and then the empty practice directory:
rm -- inspect-link.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirIf the directory is not empty, inspect the remaining files rather than deleting unknown contents.
Acknowledgements
This post was created with assistance from AI.