Conversation
Summary: Follow-up to #1854, which made `Bundler` initialisation errors reject `ready()`. Teardown still didn't account for a failed initialisation, so Metro could leak worker processes and file watchers when, for example, the transformer fails to load: - `Transformer` created its `WorkerFarm` - which spawns `jest-worker` child processes when `maxWorkers > 1` - *before* computing the transform cache key. `getTransformCacheKey` is exactly where a missing transformer or Babel preset throws (#1808), and when it did, the constructor threw away the only reference to the farm, so nothing could kill it. Computing the cache key first means any constructor failure happens before workers start. - `Bundler.end()` awaited `ready()` before ending anything, so after a failed initialisation it rejected without calling `DependencyGraph.end()`, leaving the file map's watcher, health check interval and file processor running. It now waits for initialisation to settle, ends the transformer if one was constructed, and always ends the dependency graph. - `DependencyGraph.end()` had the same shape with respect to a failed `fileMap.build()`, so it now ends the file map regardless. Initialisation errors are still surfaced through `ready()` and the reporter - `end()` now resolves either way, since teardown should complete even if startup didn't. (Before #1854, `end()` failed at the same point by dereferencing an undefined `_transformer`, so none of this is a regression.) Changelog: ``` - **[Fix]**: Release transform workers and file watchers when Metro fails to initialise ``` Test plan: New tests in `Bundler-test`, `Transformer-test` and `DependencyGraph-test` cover each case, and fail without the corresponding fix. ``` yarn jest packages/metro yarn flow check yarn lint ```
robhogan
marked this pull request as ready for review
September 24, 2026 13:22
There was a problem hiding this comment.
Copilot review overview
馃煛 Changes recommended
Ensure dependency-graph cleanup runs even when transformer teardown rejects.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
What changed in this PR
Fixes Metro resource leaks when Bundler or DependencyGraph initialization fails.
Changes:
- Defers transformer worker creation until cache-key computation succeeds.
- Makes teardown resilient to failed initialization.
- Adds regression tests for worker and file-map cleanup.
The Bundler teardown should still ensure DependencyGraph.end() runs if transformer cleanup rejects.
| File | Description |
|---|---|
packages/鈥媘etro/鈥媠rc/鈥媙ode-haste/鈥婦ependencyGraph.js |
Cleans up the file map after failed builds. |
packages/鈥媘etro/鈥媠rc/鈥媙ode-haste/鈥媉_tests__/鈥婦ependencyGraph-test.js |
Tests file-map cleanup. |
packages/鈥媘etro/鈥媠rc/鈥婦eltaBundler/鈥婽ransformer.js |
Delays worker creation until initialization succeeds. |
packages/鈥媘etro/鈥媠rc/鈥婦eltaBundler/鈥媉_tests__/鈥婽ransformer-test.js |
Tests worker suppression on cache-key failure. |
packages/鈥媘etro/鈥媠rc/鈥婤undler.js |
Handles teardown after failed initialization. |
packages/鈥媘etro/鈥媠rc/鈥媉_tests__/鈥婤undler-test.js |
Tests Bundler cleanup behavior. |
馃挕 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
If `Transformer.end()` rejected - for example, when the worker farm had already ended - `Bundler.end()` never reached `DependencyGraph.end()`, leaking the file map's watcher and timers. Wrap it in `try`/`finally` so the dependency graph is always ended, while still rejecting with the Transformer's error. Addresses review feedback on #1963.
vzaidman
approved these changes
Sep 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Follow-up to #1854, which made
Bundlerinitialisation errors rejectready().This fixes cases where a failed transformer could lead Metro to hang in a zombie state.
Teardown still didn't account for a failed initialisation, so Metro could leak worker processes and file watchers when, for example, the transformer fails to load:
Transformercreated itsWorkerFarm- which spawnsjest-workerchild processes whenmaxWorkers > 1- before computing the transform cache key.getTransformCacheKeyis exactly where a missing transformer or Babel preset throws ([Metro 0.84.x] Silent catch in Bundler._initializedPromise causes misleadingCannot read properties of undefined (reading 'transformFile')instead of the real error聽#1808), and when it did, the constructor threw away the only reference to the farm, so nothing could kill it. Computing the cache key first means any constructor failure happens before workers start.Bundler.end()awaitedready()before ending anything, so after a failed initialisation it rejected without callingDependencyGraph.end(), leaving the file map's watcher, health check interval and file processor running. It now waits for initialisation to settle, ends the transformer if one was constructed, and always ends the dependency graph.DependencyGraph.end()had the same shape with respect to a failedfileMap.build(), so it now ends the file map regardless.Initialisation errors are still surfaced through
ready()and the reporter -end()now resolves either way, since teardown should complete even if startup didn't.(Before #1854,
end()failed at the same point by dereferencing an undefined_transformer, so none of this is a regression.)Changelog:
Test plan
End to end - a project whose
transformerPathpoints at a module that throws on load, the same way a missing Babel preset does:with
maxWorkers: 2,resolver.useWatchman: falseand a no-op cache store (the transform cache key is only computed with a cache enabled). A script runs Metro from source, either throughMetro.runBuild()or throughMetro.runMetro(config, {watch: true})followed byserver.ready()andserver.end(). It then waits 1s, counts its own child processes, listsprocess.getActiveResourcesInfo(), and forces an exit if it's still alive 10s later.main(1f83bfc)Transformerchange onlyrunBuild()runMetro(),end()end()rejects. 2 worker processes and the file map's change interval left running, hangs until forced exitend()rejects. Change interval left running, hangs until forced exitend()resolves. Nothing left running, exitsSo the
Transformerreordering is enough for one-shot builds, and theend()changes are needed as well for a watching server.Unit tests - new cases in
Bundler-test,Transformer-testandDependencyGraph-test. The three covering the fixes each fail without the source changes and pass with them.