← 中文资源

简体中文版本

Node EEXIST — 写入时文件已存在

通过选择合适的 flag 并把并发创建视为正常情况,修复 mkdir、copyFile 与 write-file-atomic 抛出的 Node EEXIST。

阅读英文原文

症状

错误“Node EEXIST — 写入时文件已存在”指向一个明确的失效模式。下面依次给出症状、定位该原因的检查命令、根本原因、修复方案,以及证明修复有效的验证步骤。

可观察到的症状包括:

  • Node 抛出 “Error: EEXIST: file already exists, mkdir …”
  • 构建或部署脚本第二次运行即失败
  • 多个 worker 同时启动,都尝试创建同一目录

诊断

先在隔离环境中复现故障,再按顺序执行以下检查,逐步缩小范围。

步骤 1

node -e "require('fs').mkdir('/tmp/x/child', {}, e=>console.log(e && e.code))"

mkdir 报 EEXIST 表示目录已存在,正确做法是 recursive:true,而不是先删路径。

步骤 2

node -e "const s=require('fs').statSync('/tmp/x'); console.log(s.isDirectory())"

若路径上已存在文件,创建失败的原因不同,需单独处理。

常见诱因(按出现频率排序):

  • 调用 mkdir 时未加 recursive:true,目录已存在便视为错误
  • copyFile 使用默认 COPYFILE_EXCL 语义,拒绝覆盖
  • 并发进程争相创建同一路径

根因

Node 的 fs 操作默认严格:未加 recursive 的 mkdir、未指定 flag 的 copyFile 遇到已存在目标即抛 EEXIST。并发创建使错误时有时无,看似竞态,实为缺少幂等 flag。

修复

使用 fs.mkdir(path, { recursive: true }) 与具备预期覆盖行为的 fs.cp/copyFile,或用 try/catch 把 EEXIST 视为成功。避免先删后建,那会重新引入竞态窗口。

注意:把所有 EEXIST 都当作成功,会掩盖“普通文件占用了目录路径”的情况,导致后续写入静默失败。

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
}

验证

连续运行两次并同时用两个进程运行;两次都应退出 0,目录只存在一份。最后断言该路径确为目录。

修复后补充一条回归测试,复现原始症状,防止问题再次出现。

这类故障通常是环境问题而非纯逻辑错误,因此在缺陷记录中应写明完整命令、环境变量与工作目录。请用最小的脚本复现,待最小用例清楚后再扩大范围。为这类故障准备一份简短的排查清单,能显著减少下次排障时间。