ErrLookup › Background articles › "failed to write file", "Could not save figure", "Error saving remote file" — file write failed: causes and fixes across languages and libraries
"failed to write file", "Could not save figure", "Error saving remote file" — file write failed: causes and fixes across languages and libraries
Errors like "failed to write file", "Could not store compilation result", "Unable to write server config file", and "Error saving remote file to a temporary location" all mean the same thing: a library tried to persist data to disk and the write failed — usually because the target directory is missing or unwritable, the disk is full, the filesystem is read-only, or a lock or mount problem interrupted the I/O. This guide explains the mechanisms behind file-write failures across WebDAV servers, CLI tools, build systems, and desktop apps, and how to read the wrapped OS error to find the real cause.
Distilled from 113 documented records across 38 repositories.
Background
A failed write is one of the oldest error shapes in software: some component holds bytes it wants on disk, calls the platform's write primitive (os.WriteFile, file_put_contents, fputcsv, fs.writeFile, io.Copy, std::fs::write, or a wx SaveFile), and the underlying syscall returns an errno instead of success. What makes this family hard to search for is that nearly every library wraps that low-level failure in its own vocabulary — Go code tends to wrap the error with %w and a context phrase, PHP code throws a RuntimeException or SystemException with a generic message, Java wraps an IOException in a MojoExecutionException, and Rust panics with .expect(). The surface message almost never names the cause; the real diagnostic is the chained previous exception, the unwrapped errno, or a log line written before the user-facing error.
The causes cluster tightly despite the variety of surfaces. Disk-full (ENOSPC) is the most frequent trigger — it appears in exports streaming many rows (Coolify's CSV export, Delve's breakpoint files), in resume/state files that grow over a run, and in CI where volumes hit quota. Missing or unwritable directories come next: the target directory was never created (openapi-generator writing a collapsed spec before target/classes exists, gemini-eval's reports/ folder, Actual's userFiles directory), or permissions and ownership drifted — a file left root-owned by an earlier sudo or containerized run, a workspace owned by another user, or an open_basedir restriction silently suppressing the PHP warning so a SystemException is the first symptom. Read-only filesystems are a third cluster: immutable-distro rootfses that block the Tailscale tarball updater, container overlay layers and CI bind mounts, and volume mounts flipped to RO.
A fourth cluster is path-level problems: the destination path collides with an existing directory (Wave's WriteAppFile, SiYuan's import writes), a URL-derived filename is illegal on the host OS (SiYuan's Windows reserved-name case), a company slug produces characters the filesystem rejects, or a path is too long. Finally, mid-flight interference matters: antivirus or backup daemons holding files on Windows, a second process holding the workspace (SiYuan's filelock), sync clients racing atomic renames, a concurrent clean deleting a Gradle build directory, or a file or symlink vanishing between check and write.
How the failure is reported varies more than the cause. Many tools are deliberately defensive: writers that stage to a temp file and rename atomically (Linera's .json.new files, beads' atomicWriteFile, OpenCLI's resume file) leave the previous file untouched and safe to retry — several record pages explicitly say a retry after fixing the environment is harmless. Others degrade gracefully: Career Ops' JD cache and Gmail state persistence log a warning and fall back (to remote URLs, or to reprocessing on the next run), while octobercms's @-suppressed File::put() turns a mundane permission problem into a mysterious popup error. Some failures are asymmetric: Nextcloud's catch-all maps unknown exceptions to HTTP 500 regardless of cause, and Actual responds 500 to the client while the real errno sits in a server log line. Because of this, the universal first step is the same everywhere: chase the wrapped or previous error to the underlying errno (EACCES, EROFS, ENOSPC, EEXIST, ENOENT), because that value, not the message, tells you which cluster you are in.
Common causes
- Disk full or quota exceeded (ENOSPC). The volume holding the target file, temp directory, or build output has run out of space or inodes. Common with streaming exports, resume/state files that grow during a run, and CI runners or containers with ephemeral-storage limits. Fix by freeing space (df -h, and df -i for inodes) and retrying — most write paths are safe to retry once the space exists.
- Missing or unwritable target directory. The code opens the file before (or without ever) creating the parent directory, or the directory was removed mid-run. Typical when the tool runs from a different working directory than expected or a build phase runs before the output directory exists. Pre-create the directory (mkdir -p / os.makedirs(..., exist_ok=True)) and confirm the path resolves where you think it does.
- Permission or ownership mismatch (EACCES/EPERM). The file or directory is owned by another user (often root, from an earlier sudo or containerized run), was made read-only, or is blocked by open_basedir/LSM policy. Fix ownership (chown/chmod) and run subsequent commands as the same user so staging files stay writable.
- Read-only filesystem or mount (EROFS). The destination sits on a read-only mount: immutable-distro rootfses, container overlay layers, CI bind mounts, or a volume remounted RO after an error. Remount read-write, mount a writable volume at the path, or point the tool at a writable location instead.
- Path-level problems: illegal names, collisions, length. The derived filename is invalid on the host OS (Windows reserved names, trailing dots/spaces, characters from a URL path or company slug), collides with an existing directory, or exceeds path-length limits. Sanitize derived names and check whether the target path is occupied by a directory.
- Concurrent interference: locks, AV scans, vanishing files. Antivirus, backup, or file-sync daemons hold the file (notably on Windows), a second process holds the workspace, a concurrent clean deletes the build directory, or the file/symlink disappears between check and write. Exclude tool directories from real-time scanning, ensure single-writer access, and avoid running destructive tasks concurrently.
- Staging leftovers from atomic writes. Writers that stage to <path>.new and rename can leave behind temp files owned by another user or from a crashed run, blocking the next attempt. Treat *.new / *.json.new files as crash residue and remove them before retrying.
What usually fixes it
- Read the wrapped or chained error first: unwrap Go %w chains, read the previous exception in PHP, and find the server-side log line — the errno (EACCES, EROFS, ENOSPC, ENOENT) identifies the cause cluster, not the outer message.
- Check capacity and writability of the destination before retrying: df -h (and df -i), plus a probe like touch <dir>/.probe, covering space, permissions, and mount state in one step.
- Fix ownership and run consistently as one user: chown back directories left root-owned by sudo or containers, and avoid switching users between runs so staging files and 0600 files stay writable.
- Make the destination environment sane: pre-create parent directories, keep tool state off read-only/synced/network mounts, and use a native package flow instead of in-place updates on immutable distros.
- Retry safely after fixing the environment: atomic-rename writers leave the old file intact and best-effort writers only lose transient state, so a corrected rerun is almost always safe — but clean stale *.new staging files first.
Documented occurrences
- $e->getMessage() (nextcloud/server)
- Unable to write the CSV file. (coollabsio/coolify)
- Failed to update {file}: {message} (getgrav/grav)
- failed to write password file for user %#q: %w (lima-vm/lima)
- failed to write breakpoint to file %s:%d (go-delve/delve)
- Unable to write server config file (linera-io/linera-protocol)
- Failed to write collapsed spec {0} (OpenAPITools/openapi-generator)
- Unable to write committee description (linera-io/linera-protocol)
- apify: JD cache write failed for ${normalized.title} (${err.code || err.name}: ${err.message}); falling back to remote URL (santifer/career-ops)
- frontend capability description is required: %s (siyuan-note/siyuan)
- write data [%s] failed: %s (siyuan-note/siyuan)
- failed to write file: %w (wavetermdev/waveterm)
- Failed to write file via shell: ${normalizedPath} (firecrawl/open-lovable)
- failed to write codemod output (facebook/flow)
- ensureProxiedServerConfig: write %s: %w (gastownhall/beads)
- gmail: could not persist processed-id state — ${err.message} (santifer/career-ops)
- Could not persist Twitter likes resume state: ${error instanceof Error ? error.message : String(error)} (jackwener/OpenCLI)
- Error saving remote file to a temporary location. (octobercms/october)
- Could not store compilation result (gradle/gradle)
- Failed to write collapsed spec {0} (OpenAPITools/openapi-generator)
…and 93 more across the corpus — use search.
Honest provenance: generated on 2026-09-01 from AI-assisted analysis of the linked records. See how records are made.