Skip to content

Handle the stop that follows a failed initialization - #1896

Merged
Dirk Bäumer (dbaeumer) merged 2 commits into
microsoft:mainfrom
lagergren:fix/stop-after-failed-initialize
Oct 21, 2026
Merged

Dirk Bäumer (dbaeumer) merged 2 commits into
microsoft:mainfrom
lagergren:fix/stop-after-failed-initialize

Conversation

@lagergren

Copy link
Copy Markdown
Contributor

Fixes the "Client is not running and can't be stopped" part of #1307.

Problem

When initialization fails, the catch in BaseLanguageClient.initialize calls void this.stop(): when initializationFailedHandler returns false, when the user declines a retry, and in the default case. The client is still in ClientState.Starting at that point, so shutdown throws Client is not running and can't be stopped. It's current state is: starting. Nothing awaits that promise, so the rejection is unhandled. VS Code reports it as an unhandled error (with telemetry, as #1307 describes), and the only way an extension can avoid it is to override stop() and catch every rejection, which also hides real stop failures.

Change

The three calls go through a small stopAfterFailedInitialize() that still calls stop() but handles its rejection, the same way handleConnectionError already handles the stop() it starts: logged with this.error('Stopping server failed', error, false), without a notification.

The client still calls stop(). In my comment on #1307 I suggested releasing the connection without going through stop(), but stop() is what lets subclasses clean up: the Node client's shutdown override terminates the server process in its finally, even though the base shutdown refuses a client that is still starting. Skipping stop() would leave a server that refused initialization running.

Related issues and pull requests

None of these covers this rejection:

Tests

A refused initialization stops the client without an unhandled rejection (in Server tests) starts a client against a new failInitializeServer, which answers initialize with an error but keeps running, so the client still owns a live connection. It checks that start() rejects with the server's error, that stop() is still called once and the server process is ended (the client reaches Stopped when its connection closes), and that no rejection is left unhandled.

Validation

On macOS ARM64 with Node.js 24.21.0 and VS Code Insiders 1.143.0 (a0c652031362):

  • npm run compile passed.
  • npm --prefix client run lint and npm --prefix client-node-tests run lint passed.
  • The complete client-node-tests editor suite passed: 215 tests. Without the change to client.ts, only the new test fails, with the unhandled Client is not running and can't be stopped. It's current state is: starting rejection; unchanged main passes its 214.
  • git diff --check passed.

Copilot AI balanced review requested due to automatic review settings October 18, 2026 18:40
@lagergren

Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree company="xtclang.org"

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It modifies the core client lifecycle/async rejection-handling state machine of a widely-depended-upon library, which warrants final human sign-off despite being small and well-tested.

No open issues 🎉

What changed in this PR

This PR fixes an unhandled promise rejection that occurs when a language server refuses initialization. In BaseLanguageClient.initialize, the catch block previously called void this.stop() in three places (when initializationFailedHandler returns false, when the user declines a retry, and in the default case). Because the client is still in ClientState.Starting, the base shutdown rejects with "Client is not running and can't be stopped", and since nothing awaited the promise, VS Code surfaced it as an unhandled error (with telemetry, per #1307). The fix routes all three calls through a new private stopAfterFailedInitialize() helper that still calls stop() (preserving subclass cleanup such as the Node client terminating the server process) but handles the rejection by logging it without a notification — mirroring the existing pattern in handleConnectionError.

Changes:

  • Added stopAfterFailedInitialize() to handle stop()'s rejection after a failed initialization, replacing the three void this.stop() calls.
  • Added a failInitializeServer test server that refuses initialize but keeps its connection alive.
  • Added an integration test asserting start() rejects, stop() is called once, the client reaches Stopped, and no rejection is left unhandled.
File Description
client/​src/​common/​client.ts Introduces stopAfterFailedInitialize() and wires it into the three failed-initialization paths so the stop() rejection is logged instead of left unhandled.
client-node-tests/​src/​servers/​failInitializeServer.ts New test server that returns a ResponseError<InitializeError> from onInitialize while keeping the connection open.
client-node-tests/​src/​integration.test.ts New test verifying a refused initialization stops the client with no unhandled rejection.

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 19, 2026 17:53
@lagergren
Marcus Lagergren (lagergren) force-pushed the fix/stop-after-failed-initialize branch from 6c44640 to da1f565 Compare October 19, 2026 17:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

It is a small, focused rejection-handling fix that mirrors an established in-file pattern and is covered by a targeted regression test, with no identified correctness, API, or convention issues.

No open issues 🎉

Review effort: Balanced

When initialization fails, initialize() called `void this.stop()` while
the client was still starting. The base shutdown rejects a client that
is not running, so that promise rejected with "Client is not running and
can't be stopped" and nothing handled it. Handle the rejection, logging
it without a notification, as handleConnectionError does for the stop it
starts. The client still calls stop(), because the Node client's
shutdown override terminates the server process.

A new test starts a client against a server that refuses initialization
but keeps running, and checks that start() rejects with the server's
error, that stop() is still called and the server process is ended, and
that no rejection is left unhandled.
Copilot AI balanced review requested due to automatic review settings October 20, 2026 10:55
@lagergren
Marcus Lagergren (lagergren) force-pushed the fix/stop-after-failed-initialize branch from da1f565 to b566bfd Compare October 20, 2026 10:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The change is minimal, mirrors an existing handled-stop() pattern exactly, and is backed by a focused regression test that I traced end-to-end without finding any issues.

No open issues 🎉

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 21, 2026 08:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Although I found no issues, the change affects the client lifecycle/initialization error-handling path, so final human review is warranted before approval.

Wait — let me correct that assessment line to match my actual finding.

No open issues 🎉

Review effort: Balanced

@dbaeumer
Dirk Bäumer (dbaeumer) merged commit 4932870 into microsoft:main Oct 21, 2026
5 checks passed
@dbaeumer Dirk Bäumer (dbaeumer) added this to the 3.18.5 milestone Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants