mastra-ai/mastra · warning · StaleFileError
ESTALE
ESTALE
Error message
File was modified externally: ${path} (expected mtime ${expectedMtime.toISOString()}, actual ${actualMtime.toISOString()}) What it means
writeFile throws StaleFileError (code ESTALE) when options.expectedMtime is provided and the file's current modification time differs from the expected value. This is optimistic concurrency control: it detects that the file was modified by someone else between your read and your write, preventing silent lost updates. If the file does not exist, no conflict is possible and the write proceeds.
Source
Thrown at packages/core/src/workspace/filesystem/local-filesystem.ts:448
throw new DirectoryNotFoundError(parentPath);
}
throw error;
}
}
if (options?.recursive !== false) {
const dir = nodePath.dirname(absolutePath);
await fs.mkdir(dir, { recursive: true });
}
// Optimistic concurrency: reject if file was modified since caller last read it
if (options?.expectedMtime) {
try {
const currentStat = await fs.stat(absolutePath);
// Compare via Date objects — Node's stats.mtime applies internal
// rounding that can diverge from Math.floor(stats.mtimeMs).
if (currentStat.mtime.getTime() !== options.expectedMtime.getTime()) {
throw new StaleFileError(inputPath, options.expectedMtime, currentStat.mtime);
}
} catch (error: unknown) {
if (error instanceof StaleFileError) throw error;
// File doesn't exist yet — no conflict possible, proceed with write
if (!isEnoentError(error)) throw error;
}
}
// Use 'wx' flag for atomic overwrite check (avoids TOCTOU race)
const writeFlag = options?.overwrite === false ? 'wx' : 'w';
try {
await fs.writeFile(absolutePath, this.toBuffer(content), { flag: writeFlag });
} catch (error: unknown) {
if (options?.overwrite === false && isEexistError(error)) {
throw new FileExistsError(inputPath);
}
throw error;
}View on GitHub (pinned to 75dd419e61)
Solutions
- Re-read the file (getting fresh content and mtime via stat), merge/re-apply your change, and retry the write with the new expectedMtime.
- If last-writer-wins is acceptable, omit expectedMtime entirely.
- Use the file-write-lock utilities (file-write-lock.ts) or an external lock to serialize writers.
- Catch StaleFileError and surface a conflict so the calling agent can re-base instead of overwriting.
Example fix
// before
await fs.writeFile('state.json', next, { expectedMtime: oldMtime }); // StaleFileError if changed
// after
let success = false;
while (!success) {
const { content, mtime } = await readWithMtime('state.json');
try {
await fs.writeFile('state.json', merge(content, next), { expectedMtime: mtime });
success = true;
} catch (e) {
if (!(e instanceof StaleFileError)) throw e;
}
} Defensive patterns
Strategy: retry
Validate before calling
const s = await ws.stat(p); // use s.modifiedAt (fresh) as expectedMtime immediately before writing; // if the file changed since your read, re-read and merge first
Type guard
import { StaleFileError } from '@mastra/core/workspace/errors';
function isStaleFileError(e: unknown): e is StaleFileError {
return e instanceof StaleFileError ||
(e instanceof Error && 'code' in e && (e as { code?: string }).code === 'ESTALE');
} Try / catch
for (let attempt = 0; attempt < 3; attempt++) {
const s = await ws.stat(p);
const fresh = await ws.readFile(p, { encoding: 'utf8' });
try {
await ws.writeFile(p, merge(fresh, myChange), { expectedMtime: s.modifiedAt });
break;
} catch (e) {
if (!isStaleFileError(e) || attempt === 2) throw e;
}
} Prevention
- Capture expectedMtime as late as possible — immediately before the write.
- Keep the read-modify-write window short; re-read inside the same step that writes.
- Serialize writers with the file-write-lock utilities or an external lock.
- Only use expectedMtime when lost updates matter; otherwise omit it.
When it happens
Trigger: Two agents/processes edit the same file concurrently; writeFile carries an expectedMtime captured from an earlier stat/read but another writer committed first; clock/filesystem rounding causes mtime mismatch on filesystems with coarse timestamps; caller reuses a stale mtime from a cached FileStat.
Common situations: Multi-agent workflows where several agents write shared state files; a human editor saved the file while an agent run was in flight; long-running job re-tries a write with an old expectedMtime after the file legitimately changed.
Related errors
- EEXIST
- MASTRA_ENTRY_FILE_NOT_FOUND
- Failed to copy studio assets from "${studioSource}" to "${st
- Failed to copy studio assets from "${studioSource}" to "${st
- Failed to read studio routes manifest at "${manifestPath}":
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/f802f3d748c236ac.
Report an issue: GitHub.