NodeJSFile Stats & Metadata

File Stats & Metadata

Beyond a file's contents, the filesystem tracks metadata: its size, type (file/directory/symlink), timestamps, and permissions. fs.stat retrieves all of it as a Stats object. This is how you answer "how big is it?", "is this a directory?", and "when was it last modified?" — and how you check existence without the racy exists.

Getting stats

JS
import { stat } from 'node:fs/promises'

const s = await stat('report.pdf')

s.size            // bytes
s.isFile()        // true
s.isDirectory()   // false
s.mtime           // Date — last modified
s.birthtime       // Date — created
The Stats object

Member

Meaning

size

File size in bytes

isFile()

Regular file?

isDirectory()

Directory?

isSymbolicLink()

Symlink? (only via lstat — see below)

mtime / mtimeMs

Last content modification (Date / epoch ms)

atime

Last access time

ctime

Last inode change (permissions, rename) — not creation

birthtime

Creation time (where the OS supports it)

mode

Permission bits (see below)

`ctime` is NOT creation time
A common trap: `ctime` means *change time* — when the inode metadata last changed (a chmod, a rename, a link count change), **not** when the file was created. Creation time is `birthtime`, and even that isn't reliably tracked on all filesystems. For "when was this made?", prefer `birthtime` but don't depend on it cross-platform.
stat vs lstat vs fstat

Function

Follows symlinks?

Use for

stat(path)

Yes — stats the target

Normal case

lstat(path)

No — stats the link itself

Detecting symlinks

fstat(fd)

N/A — stats an open file descriptor

After you opened a file

Use `lstat` to detect symlinks
`stat` transparently follows a symlink and reports the file it points to — so `isSymbolicLink()` on a `stat` result is always `false`. To tell whether the path *is* a link (e.g. when walking a tree to avoid loops), use `lstat`, which describes the link itself.
Existence check — the right way

There's no good "does it exist?" boolean — and that's intentional. Use access (or just attempt your real operation and catch ENOENT):

JS
import { access, constants } from 'node:fs/promises'

async function exists(path) {
  try { await access(path); return true }
  catch { return false }
}

// Check specific permissions:
await access('script.sh', constants.R_OK | constants.X_OK)  // readable & executable?
Don't check-then-act — it's a race condition
Using `exists`/`access` to gate a later operation is a **TOCTOU** race: the file can change between the check and the use. Prefer attempting the actual operation (open, read, write) and handling its error. `access` is fine for a *diagnostic* ("warn if config is missing at startup"), not as a guard before every read.
Reading permissions from mode

The mode field packs the Unix permission bits. Mask and format them to the familiar octal:

JS
import { stat } from 'node:fs/promises'

const s = await stat('script.sh')
const octal = (s.mode & 0o777).toString(8)
console.log(octal)   // e.g. '755'
755
Permissions are a Unix concept
The `mode` bits (read/write/execute for owner/group/other) come from Unix. On Windows they're emulated and largely meaningless — don't build cross-platform logic on exact permission values. Change them with `chmod(path, 0o644)` and ownership with `chown` (Unix only).
A practical example: human-readable size

JS
import { stat } from 'node:fs/promises'

function formatBytes(n) {
  const units = ['B', 'KB', 'MB', 'GB', 'TB']
  let i = 0
  while (n >= 1024 && i < units.length - 1) { n /= 1024; i++ }
  return `${n.toFixed(1)} ${units[i]}`
}

const { size } = await stat('video.mp4')
console.log(formatBytes(size))   // e.g. '847.3 MB'
Next
Build, resolve, and normalize the paths you feed all these functions: [Working with File Paths](/nodejs/file-paths).