Skip to content

ng update: formatFiles fails with spawn ENAMETOOLONG/E2BIG on large repos (all files passed as one argv) #33597

Description

@mdlufy

Which @angular/* package(s) are the source of the bug?

@angular/cli

Is this a regression?

Yes — introduced by feat(@angular/cli): integrate file formatting into update migrations (91b9d28, Jan 2026). Migrations did not run Prettier before this.

Description

After a migration touches many files, the CLI runs Prettier over all changed files in a single spawn. On a large repo (e.g. an Nx monorepo with 1000+ libs / thousands of changed files) the combined argument list exceeds the OS command-line limit and the spawn is rejected:

WARNING: Formatting of files failed with the following error: spawn ENAMETOOLONG

The migration itself succeeds (the error is caught and only logged), but none of the changed files get formatted — so on big repos the new formatting step effectively never runs, which is exactly where it is most useful.

Root cause

packages/angular/cli/src/utilities/prettier.tsformatFiles spreads the entire files set into one execFile argument list:

await execFileAsync(
  process.execPath,
  [prettierCliPath, '--write', '--no-error-on-unmatched-pattern', '--ignore-unknown', ...files],
  { cwd, shell: false },
);

When files is large, argv exceeds the platform limit:

  • Windows → ENAMETOOLONG (CreateProcess command line capped at 32,767 chars)
  • Linux / macOS → E2BIG (total argv+envp over ARG_MAX)

shell: false does not help — the limit is on the execve/CreateProcess argument vector itself. Confirmed still present on main.

Please provide a link to a minimal reproduction of the bug

Reproduces exactly what formatFiles builds — no monorepo checkout needed. The spawn fails at the OS level before Prettier even starts, so the paths don't need to exist:

// repro.mjs — run: node repro.mjs
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { createRequire } from 'node:module';

const execFileAsync = promisify(execFile);
const require = createRequire(import.meta.url);
const prettierCli = require.resolve('prettier/bin/prettier.cjs');

const files = Array.from(
  { length: 25_000 },
  (_, i) => `libs/feature-${i % 200}/src/lib/components/very/deeply/nested/widget-${i}.component.ts`,
);

try {
  await execFileAsync(process.execPath, [prettierCli, '--write', ...files], { shell: false });
} catch (error) {
  console.error(error.code, error.syscall); // E2BIG spawn (linux/mac) | ENAMETOOLONG spawn (windows)
}

Confirmed locally (Node v26, macOS, ARG_MAX 1 MiB): 25,000 paths ≈ 2 MB of argv → spawn E2BIG. The same overflow surfaces as spawn ENAMETOOLONG on Windows, which is what users hit in the wild (see references).

Please provide the exception or error you saw

spawn ENAMETOOLONG

(or spawn E2BIG on Linux/macOS)

Proposed fix

Batch the file list so each spawn's argument length stays under a conservative cross-platform budget, and run Prettier once per batch:

export async function formatFiles(cwd: string, files: Set<string>): Promise<void> {
  if (!files.size) {
    return;
  }

  // ...unchanged prettierCliPath resolution...

  if (!prettierCliPath) {
    return;
  }

  const baseArgs = [
    prettierCliPath,
    '--write',
    '--no-error-on-unmatched-pattern',
    '--ignore-unknown',
  ];

  // Keep each spawn's argv well under the platform command-line limit
  // (~32 KiB on Windows, larger on POSIX) to avoid E2BIG / ENAMETOOLONG when a
  // migration touches thousands of files.
  const MAX_ARG_LENGTH = 32_000;
  const baseLength = baseArgs.reduce((total, arg) => total + arg.length + 1, 0);

  let batch: string[] = [];
  let batchLength = baseLength;

  const flush = async (): Promise<void> => {
    if (batch.length) {
      await execFileAsync(process.execPath, [...baseArgs, ...batch], { cwd, shell: false });
      batch = [];
      batchLength = baseLength;
    }
  };

  for (const file of files) {
    const fileLength = file.length + 1;
    if (batch.length && batchLength + fileLength > MAX_ARG_LENGTH) {
      await flush();
    }
    batch.push(file);
    batchLength += fileLength;
  }

  await flush();
}

32_000 is intentionally conservative (below the Windows 32,767 limit, far below POSIX ARG_MAX), leaving headroom for the environment block. Verified on the repro above: the 25,000-file case runs across ~63 spawns with no error. Happy to send a PR.

Notes

Please provide the environment you discovered this bug in

Angular CLI: 21.x (also reproduced on main)
Node: 26.0.0
OS: darwin (also reported on Windows)
Prettier: 3.x

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions