Node EEXIST — File Already Exists When Writing
Fix Node EEXIST errors from mkdir, copyFile, and write-file-atomic by choosing the right flag and treating concurrent creation as normal.
Symptoms
- Node throws "Error: EEXIST: file already exists, mkdir ..."
- A build or deploy script fails when run a second time
- Multiple workers start at once and all try to create the same directory
Possible causes
- mkdir is called without recursive:true, so an existing directory is an error
- copyFile uses the default COPYFILE_EXCL flag semantics, refusing to overwrite
- Concurrent processes race to create the same path
Diagnosis steps
Identify the failing operation and flag
node -e "require('fs').mkdir('/tmp/x/child', {}, e=>console.log(e && e.code))"Interpretation
EEXIST with mkdir means the directory exists; the fix is recursive:true, not deleting the path.
Check whether the path is a file or a directory
node -e "const s=require('fs').statSync('/tmp/x'); console.log(s.isDirectory())"Interpretation
If a file occupies the directory path, creation fails for a different reason and must be handled explicitly.
Root cause
Node fs operations are strict by default: mkdir without recursive and copyFile without flags refuse to act on an existing target and raise EEXIST. Concurrent creators make the error intermittent, which looks like a race but is really a missing idempotency flag.
Fix
Use fs.mkdir(path, { recursive: true }) and fs.cp/copyFile with the intended overwrite mode, or guard with a try/catch that treats EEXIST as success. Avoid delete-then-create, which reintroduces a race window.
Risk note
Treating every EEXIST as success hides the case where a regular file occupies the directory path and writes will silently fail.
Verify the fix
Run the script twice and from two processes at once; both runs should exit 0 and the directory should exist exactly once. Assert the path is a directory afterwards.
Symptoms
The error “Node EEXIST — File Already Exists When Writing” points at a specific failure mode. This guide documents the symptoms you can observe, the commands that isolate the cause, the root cause, the fix, and the checks that prove the fix works.
Observed symptoms include:
- Node throws “Error: EEXIST: file already exists, mkdir …”
- A build or deploy script fails when run a second time
- Multiple workers start at once and all try to create the same directory
Reproduce and Diagnose
Reproduce the failure in isolation before changing any code. Work through the checks below in order; each one narrows the search space.
Step 1: Identify the failing operation and flag
node -e "require('fs').mkdir('/tmp/x/child', {}, e=>console.log(e && e.code))"
EEXIST with mkdir means the directory exists; the fix is recursive:true, not deleting the path.
Step 2: Check whether the path is a file or a directory
node -e "const s=require('fs').statSync('/tmp/x'); console.log(s.isDirectory())"
If a file occupies the directory path, creation fails for a different reason and must be handled explicitly.
Likely contributors, in order of frequency:
- mkdir is called without recursive:true, so an existing directory is an error
- copyFile uses the default COPYFILE_EXCL flag semantics, refusing to overwrite
- Concurrent processes race to create the same path
Root Cause
Node fs operations are strict by default: mkdir without recursive and copyFile without flags refuse to act on an existing target and raise EEXIST. Concurrent creators make the error intermittent, which looks like a race but is really a missing idempotency flag.
The Fix
Use fs.mkdir(path, { recursive: true }) and fs.cp/copyFile with the intended overwrite mode, or guard with a try/catch that treats EEXIST as success. Avoid delete-then-create, which reintroduces a race window.
Watch out: Treating every EEXIST as success hides the case where a regular file occupies the directory path and writes will silently fail.
import { mkdir, cp } from 'node:fs/promises';
await mkdir('/var/app/cache', { recursive: true }); // idempotent
try {
await cp('src', '/var/app/cache/src', { recursive: true, force: true });
} catch (err) {
if (err.code !== 'EEXIST') throw err; // EEXIST here is benign
}
Verify and Prevent
Run the script twice and from two processes at once; both runs should exit 0 and the directory should exist exactly once. Assert the path is a directory afterwards.
Once verified, add a regression test that reproduces the original symptom so the failure cannot silently return.
This class of failure is usually environmental rather than pure logic, so capture the exact command, the environment variables, and the working directory in your bug report. Reproduce it in the smallest possible script, then widen the scope only once the minimal case is understood. A short written checklist for this failure mode saves hours the next time it appears.