Fixing "Error: spawn EBADF" in Angular 22.2 ng serve and ng test
After updating a project to Angular 22.2.0 you may see ng serve -o fail to open the browser, or ng test start in watch mode and run no tests at all. Both come with the same error: spawn EBADF.
The culprit is a single option in angular.json, and removing it fixes both.
Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.
Symptoms
ng serve -o (or npm start in many projects) compiles fine, but the browser never opens:
Error: spawn EBADF
at ChildProcess.spawn (node:internal/child_process:458:11)
at Object.spawn (node:child_process:813:9)
at baseOpen (.../vite/dist/node/chunks/node.js:15099:34)
at startBrowserProcess (.../vite/dist/node/chunks/node.js:15220:3)
ng test in a terminal (watch mode is the default there) reports no tests, with:
Serialized Error: { errno: -9, code: 'EBADF', syscall: 'spawn' }
Error: browserType.launch: spawn EBADF # when "browsers" is configured
In a larger workspace, ng serve or ng build --watch may not get that far. They crash right after the first build with a different error:
Watch mode enabled. Watching for file changes...
Error: EMFILE: too many open files, watch '.../dist/...'
at FSWatcher._handleError (.../chokidar/index.js:558:18)
code: 'EMFILE',
ng build and ng test --watch=false keep working. The same project on Angular 22.1.8 has none of these problems.
Cause
Check the build options in angular.json:
"build": {
"builder": "@angular/build:application",
"options": {
"preserveSymlinks": true
}
}
In @angular/build 22.2.0, preserveSymlinks: true switches the watcher to follow symlinks and stops excluding node_modules from watching. On macOS this watcher holds one open file descriptor per watched file, so the process ends up with tens of thousands of them: about 18,000 on a fresh ng new project, and about 29,000 on our admin template. In 22.1.8 the same configuration keeps under 30.
With that many descriptors open, every child process the CLI starts with piped stdio fails with EBADF. That covers the browser opener behind -o, the Vitest worker pool and Playwright’s browser launch.
Which error you get depends on how many files the project contains. macOS allows 61,440 open files per process (sysctl kern.maxfilesperproc). Below that, the watcher starts and the child processes fail with EBADF. Above it, the watcher itself fails with EMFILE. Build output counts too, because the watcher only skips the current outputPath. Old builds in dist/ from other configurations are all watched. In our PRO template, about 39,000 files in node_modules plus about 63,000 in a local dist/ full of demo builds went over the limit. A clean checkout of the same template gave EBADF instead.
Many projects carry preserveSymlinks: true over from older templates without ever using npm link. Until 22.2.0 it cost nothing in watch mode, so it went unnoticed.
Fix
Remove preserveSymlinks from angular.json, or set it to false:
"options": {
- "preserveSymlinks": true,
"outputPath": "dist/my-app",
Restart ng serve or ng test and both work again. The same change fixes EMFILE, so you don’t need to clean dist/ or raise any system limits.
Keep the option only if you actually develop against symlinked packages (npm link, local workspace links). In that case pin @angular/build to 22.1.x until a fixed release ships. If you only need symlinks now and then, leave the option out and pass --preserve-symlinks to ng build when you need it. ng serve does not accept that flag.
CoreUI Angular Admin Templates
The free CoreUI Angular Admin Template had preserveSymlinks: true in its angular.json and was hit by this after the Angular 22.2.0 update. Version 5.7.32 removes the option. If you started a project from an earlier version, apply the one-line fix above to your own angular.json.
The CoreUI PRO Angular Admin Template carried the same option up to version 5.7.23. Its @angular/build range resolves to 22.2.0 on a fresh install, so those versions show the same errors. If you keep builds of several themes in dist/, expect EMFILE rather than EBADF. Version 5.7.32 removes the option. If your project started from an earlier version, apply the same one-line fix.



