vitest-dev/vitest · error · Error

Couldn't write file to fs

Error message

Couldn't write file to fs

What it means

Thrown by writeScreenshot after both assertBrowserApiWrite and assertBrowserFileAccess have already passed, so it indicates a genuine filesystem failure during mkdir(dirname) or writeFile. The original error is preserved on the `cause` property. It is a defensive wrapper that normalizes any low-level FS error (EACCES, ENOSPC, EISDIR, ENOENT for an unresolvable parent) into a single human-readable message.

Solutions

  1. Inspect err.cause for the real POSIX error code (EACCES/ENOSPC/EISDIR) and address that specifically.
  2. Free disk space or raise the CI runner disk quota if err.cause.code === 'ENOSPC'.
  3. Change the screenshot output path to a writable location inside the project root, or grant write permission to the target directory.
  4. Confirm the path does not point to an existing directory or a file owned by another user.
  5. If running in a container, verify the mounted volume is writable by the Vitest process user.

Example fix

// before
await writeFile(path, image)
// after - surface the real cause and pick a writable, existing parent
try {
  await mkdir(dirname(path), { recursive: true })
  await writeFile(path, image)
}
catch (cause) {
  throw new Error(`Couldn't write screenshot to ${path}: ${(cause as NodeJS.ErrnoException).code}`, { cause })
}
Defensive patterns

Strategy: try-catch

Validate before calling

import { access, constants } from 'node:fs/promises'
async function canWriteTo(dir: string) {
  try { await access(dir, constants.W_OK); return true } catch { return false }
}

Type guard

function isNodeErrno(e: unknown): e is NodeJS.ErrnoException {
  return e instanceof Error && typeof (e as NodeJS.ErrnoException).code === 'string'
}

Try / catch

try { await browserCommandThatWritesScreenshot(path) }
catch (e) {
  const code = (e as NodeJS.ErrnoException).cause && ((e as any).cause as NodeJS.ErrnoException).code
  if (code === 'ENOSPC') /* free disk */
  else if (code === 'EACCES') /* fix perms */
  else throw e
}

Prevention

When it happens

Trigger: Calling a screenshot-matcher command (e.g. expect(...).toMatchScreenshot with an output path) where the destination directory cannot be created or the file cannot be written. Concrete causes: read-only filesystem, ENOSPC (disk full), EACCES on the target directory, a path that resolves to an existing directory (EISDIR), or a symbolic-link loop that mkdir cannot resolve.

Common situations: Running browser screenshot tests in a sandboxed/CI container with a read-only mount; specifying a screenshot path outside the project root that the OS refuses to create; disk-full CI runners; Windows path with illegal characters; a relative path that resolves to a directory slot already occupied.

Related errors


AI-assisted analysis of vitest-dev/vitest@1fa9837ec2 (2026-08-11). Data as JSON: /api/errors/abec29187310c33f. Report an issue: GitHub.

Appendix: source

Thrown at packages/browser/src/node/commands/screenshotMatcher/index.ts:571

    target,
  })

  return {
    buffer: screenshot,
    image: await codec.decode(screenshot, {}),
  }
}

/** Writes encoded images to disk, creating parent directories as needed. */
async function writeScreenshot(path: string, image: TypedArray, project: TestProject) {
  try {
    assertBrowserApiWrite(project, path)
    assertBrowserFileAccess(project, path)
    await mkdir(dirname(path), { recursive: true })
    await writeFile(path, image)
  }
  catch (cause) {
    throw new Error('Couldn\'t write file to fs', { cause })
  }
}

View on GitHub (pinned to 1fa9837ec2)