Skip to content

fix(core): run the shutdown sequence after a failed init - #17966

Merged
kamilmysliwiec merged 1 commit into
masterfrom
fix/close-after-failed-init
Oct 1, 2026
Merged

kamilmysliwiec merged 1 commit into
masterfrom
fix/close-after-failed-init

Conversation

@kamilmysliwiec

Copy link
Copy Markdown
Member

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Other... Please describe:

What is the current behavior?

When init() of an application context fails, closing it afterwards tears nothing down. close() rejects with the init error, and no onModuleDestroy, beforeApplicationShutdown or onApplicationShutdown hook runs. Whatever the partial startup acquired stays open: a connection pool a module created before another provider's onModuleInit() threw, timers, sockets. A shutdown signal does the same: it logs the init error as "Error happened during shutdown" and exits with code 1, without running a hook.

const moduleRef = await Test.createTestingModule({ imports: [AppModule] }).compile();

await moduleRef.init().catch(() => {}); // a provider's onModuleInit() throws
await moduleRef.close();
// Rejects with the init error. The database pool is never ended,
// and the test runner hangs on it or leaks it.

The cause is in runShutdownSequence(). Since the change that made a shutdown wait for an initialization in flight, it starts with await this.initializationPromise. After a failed init(), that promise is rejected, so the shutdown rethrows the init error before its first step.

NestApplication doesn't have the problem, because its own init() never sets that promise. So a failed app.init() followed by app.close() already tears down. Standalone application contexts (NestFactory.createApplicationContext()) and TestingModule#init() do have it.

What is the new behavior?

A shutdown still waits for an initialization in flight to settle, but it no longer adopts its failure. The init error has already reached the caller of init(). close() and the signal handlers then run the whole teardown and release what the startup acquired, as they do for HTTP applications. A signal that arrives while an initialization that later fails is still running waits for it, then tears down.

Tests:

  • NestApplicationContext:
    • close() after a failed init() runs the destroy and both shutdown hooks;
    • a signal after a failed init() runs the shutdown hooks and re-raises the signal, instead of exiting with 1;
    • a signal during an init() that later fails waits for it to settle, then runs the hooks.
  • Integration: a TestingModule whose init() fails calls onApplicationShutdown on close().

Each new test fails without the fix.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

close() after a failed init() now resolves if the teardown succeeds, instead of rejecting with the init error again. Code that awaited close() to rethrow the init error would get it from init() already.

`close()` and the shutdown signal handlers awaited the initialization promise as it was, so after a
failed `init()` they rethrew the init error and skipped the whole teardown: `onModuleDestroy`,
`beforeApplicationShutdown`, `dispose()` and `onApplicationShutdown` never ran, and what the
partial startup had acquired (connection pools, timers, sockets) leaked. A signal also logged the
init error as a shutdown error and exited with code 1.

A shutdown still waits for an initialization in flight to settle, but no longer adopts its failure.
Application contexts and testing modules now behave like HTTP applications, whose `init()` never
set that promise.
@kamilmysliwiec
kamilmysliwiec merged commit 4b596a5 into master Oct 1, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant