Skip to content

fix(create-electron-app): surface dependency install failures during init - #4348

Draft
erickzhao wants to merge 1 commit into
nextfrom
fix/init-silent-dependency-install-failures
Draft

fix(create-electron-app): surface dependency install failures during init#4348
erickzhao wants to merge 1 commit into
nextfrom
fix/init-silent-dependency-install-failures

Conversation

@erickzhao

Copy link
Copy Markdown
Member

Summary

The three dependency-install tasks in init were marked exitOnError: false. listr2 treats that as "mark this task FAILED but do not rethrow", so a failed install never propagated — init resolved, the CLI exited 0, and the parent tasks still rendered a green checkmark over the failed child.

The result is a scaffolded app that is quietly broken. Passing an unresolvable version (which clears semver.clean and then fails at install time) leaves electron absent from both devDependencies and node_modules, while still exiting 0:

✖ Installing common dependencies [FAILED: ...No candidates found]
✔ Finalizing dependencies
✔ Installing template dependencies
EXIT CODE: 0

With an unreachable registry it's worse — zero dependencies, no node_modules at all, still exit 0.

Via the programmatic API the failure is completely invisible: interactive defaults to false, so silentRendererCondition is true, there is no line at all, and init() simply resolves.

The relevant listr2 code (Task.run):

if (this.listr.options.exitOnError !== false && await assertFunctionOrSelf(this.task?.exitOnError, context) !== false) {
  wrapper.report(error, "HAS_FAILED"); this.close(); throw error;
} else if (!this.hasSubtasks()) {
  wrapper.report(error, "HAS_FAILED_WITHOUT_ERROR");  // no rethrow
}

The error wasn't recoverable after the fact either: report() pushes onto this.task.listr.errors — the innermost nested list — so top-level runner.errors.length === 0 and a post-run error check would not have caught it.

Fix

Removed the option, restoring the default (exitOnError: true). This matches the initLink sibling in the same list, which already sets exitOnError: true explicitly. Failures now travel the same path as every other init error (e.g. an unknown template) and exit non-zero.

I fixed all three sites rather than only the initNPM one. The two template-dependency tasks are the same defect and are reachable today, since the webpack and vite templates have non-empty dependency lists. Happy to split them out if you'd prefer a narrower diff.

Background

This isn't a deliberate design choice. exitOnError: false arrived as a list-level option in the async-ora → listr2 port (#3022); the pre-port code propagated these errors. #3219 — titled "fix(core): silent failures when linking forge dependencies" — then pushed the option down onto individual tasks and set exitOnError: true for initLink only, so this exact bug class was recognized and fixed for linking while the installs were left silent.

Testing

Added packages/external/create-electron-app/spec/fast/init.spec.ts. Verified it genuinely catches the regression: with the fix reverted, 2 of the 3 tests fail with promise resolved "undefined" instead of rejecting.

  • yarn test:fast — 56 files, 412 passed
  • yarn lint:js — clean
  • Manual end-to-end: create-electron-app <dir> --electron-version 999.999.999 now exits 1 instead of 0

Note for a follow-up

Init failures surface through terminate.ts's unhandledRejection handler, so the output reads "An unhandled rejection has occurred inside Forge". That wording is misleading, but it's pre-existing and shared by every init failure path (commander's .action() promise is never awaited) — worth a separate cosmetic fix rather than widening this one.

🤖 Generated with Claude Code

…init

The three dependency-install tasks in `init` were marked `exitOnError: false`.
listr2 treats that as "mark this task FAILED but do not rethrow", so a failed
install never propagated: `init` resolved, the CLI exited 0, and the parent
tasks still rendered a green checkmark over the failed child.

The result is a scaffolded app that is quietly broken. Passing an unresolvable
version, for example, leaves `electron` absent from both `devDependencies` and
`node_modules` while still exiting 0:

    ✖ Installing common dependencies [FAILED: ...No candidates found]
    ✔ Finalizing dependencies
    ✔ Installing template dependencies
    exit 0

With an unreachable registry it is worse — no dependencies and no
`node_modules` at all, still exit 0. Via the programmatic API the failure is
entirely invisible, since `interactive` defaults to false and the renderer is
silent, so `init()` resolves with no output whatsoever.

The error was not recoverable after the fact either. listr2's `report()` pushes
onto the *nested* list's `errors` array, so the top-level `runner.errors` stays
empty and a post-run check would not have caught it.

Removing the option restores the default (`exitOnError: true`), matching the
`initLink` sibling task, which already sets it explicitly. Failures now travel
the same path as every other init error and exit non-zero.

All three sites are fixed rather than only the `initNPM` one: the two template
dependency tasks are the same defect and are reachable today, since the webpack
and vite templates have non-empty dependency lists.

Note that #3219 fixed this exact class of bug for `initLink` — it moved the
option from the list level onto individual tasks and set `exitOnError: true`
for linking, but left the installs silent. The option originally arrived in the
async-ora to listr2 port (#3022); the pre-port code propagated these errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the next label Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant