ErrLookupBackground 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

What usually fixes it

Documented occurrences

…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.