Skip to content

Guard terminal resize/dispose race against xterm.js dimension getters#314795

Merged
bryanchen-d merged 1 commit intomainfrom
bryanchen-d/terminal-resize-dispose-race
May 7, 2026
Merged

Guard terminal resize/dispose race against xterm.js dimension getters#314795
bryanchen-d merged 1 commit intomainfrom
bryanchen-d/terminal-resize-dispose-race

Conversation

@bryanchen-d
Copy link
Copy Markdown
Contributor

What

Guards three terminal resize/dispose race paths that throw Cannot read properties of undefined (reading 'dimensions') from xterm.js' RenderService.get dimensions() after the renderer is disposed.

Why

PR #313817 bumped @xterm/* to beta.213 and resolved the OverviewRulerRenderer dispose race (bucket 8612c55d, ✅ zero hits on post-fix insider builds). However three sibling buckets keep firing on the latest insiders (1.120.0-insider 79d6cea3 / 0aed0a9b):

Bucket Caller First seen on post-fix insider
aaa283a2 _resizeYCallback from configChange → setVisible_resize 161 hits / 98 users
4826565b debounced _debounceResizeX timer 909 hits / 497 users
1e83a096 _resizeBothCallback from _onProcessExitdisposesetVisible(false)_resize 291 hits / 98 users

All three reach RenderService.get dimensions() inside xterm.js, which dereferences this._renderer.value (undefined post-dispose) and throws synchronously. The optional chaining on rawXterm.dimensions?.css.canvas.width doesn't help because the getter itself throws before the ?. short-circuits.

The upstream 08ae141 patch added isDisposed guards at the OverviewRuler call sites — not inside the getter — so the symmetrical fix here is to guard the call sites VS Code owns.

Changes

terminalInstance.ts

  • Bail out of the three resize closures passed to TerminalResizeDebouncer when this.isDisposed.
  • _updatePtyDimensions: early-return when disposed; defensively try/catch around rawXterm.dimensions so a future renderer-dispose race fails silently with undefined dimensions instead of bubbling as an unhandled error.

terminalResizeDebouncer.ts

  • resize / flush: bail out when this._store.isDisposed.
  • runWhenWindowIdle callbacks: re-check disposed state before invoking the resize callback (the idle callback can fire after MutableDisposable.clear for a different timing reason).
  • _debounceResizeX: re-check disposed state. The @debounce(100) decorator schedules a setTimeout that is not tied to the disposable store, so it can fire after the terminal/xterm is gone — this is the direct cause of bucket 4826565b.

How to test

  1. Open a terminal, type or generate >200 lines of output.
  2. Trigger the dispose-during-resize patterns:
    • Quickly close (Ctrl+Shift+W) a terminal while resizing the window.
    • Run a config change (e.g. toggle terminal.integrated.fontFamily) immediately after killing a terminal.
    • Kill a long-running shell from within the terminal (exit) while the panel is mid-resize.
  3. Verify no unhandled dimensions TypeErrors in DevTools console.
  4. Telemetry: monitor buckets aaa283a2, 4826565b, 1e83a096 over the next two insider builds — expect zero new occurrences.

Risk

  • All guards are early-returns / try-catch around accessors — no behavior change in the live (non-disposed) path.
  • Defensive try/catch in _updatePtyDimensions accepts a single missed dimension update if the renderer is mid-dispose; this is preferable to throwing and losing the entire reconnect/restore flow.
  • No test changes; the races are timing-dependent and hard to reproduce deterministically. Telemetry will be the verification signal.

Refs #303546, #313817

After bumping xterm.js to beta.213 (#313817), three error buckets keep firing the same Cannot read properties of undefined (reading 'dimensions') TypeError on the latest insider builds (1.120.0-insider 79d6cea / 0aed0a9):

- aaa283a2 - resize -> _resizeYCallback -> _updatePtyDimensions, triggered by config-change -> setVisible -> _resize. - 4826565b - debounced _debounceResizeX timer firing after the renderer is gone. - 1e83a096 - _onProcessExit -> dispose -> setVisible(false) -> _resize -> _resizeBothCallback.

All three reach RenderService.get dimensions() in xterm.js, which dereferences this._renderer.value (undefined post-dispose) and throws synchronously. The optional chaining on rawXterm.dimensions doesn't help because the getter itself throws.

The upstream beta.213 fix added isDisposed guards at the OverviewRuler call sites, not inside the getter, so the correct symmetrical fix here is to guard the call sites that VS Code owns:

* terminalInstance.ts: bail out of the resize closures and _updatePtyDimensions when the instance is already disposed; defensively wrap the dimensions read in try/catch so a future renderer-dispose race fails silently instead of bubbling as an unhandled error.

* terminalResizeDebouncer.ts: bail out of resize/flush and the runWhenWindowIdle / @debounce-scheduled callbacks when the debouncer's store is disposed - the @debounce decorator schedules a setTimeout that isn't tied to the disposable store.

Refs #303546, #313817
// that here so the optional chaining short-circuits to undefined.
let pixelWidth: number | undefined;
let pixelHeight: number | undefined;
try {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This just looks like we want to swallow known error instead of actually addressing root cause.

Seems like actual throw is from xterm not vscode.

@bryanchen-d What is the repro step for this dimension error? Is it causing breakage in terminal behavior?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It is hard to find the repro step. Essentially the error comes from calling term.dimensions after term.dispose(), so user shouldn't see visible breakage since the terminal has been disposed.

I think the try-catch here is no-harm here, but I'd agree we can remove it since we have already check isDisposed above.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It will be harm if the root cause is somewhere else and this would prevent us from root-causing the issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's fair, I made a follow up PR to revert that part, can you review?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

yes, already approved.

@bryanchen-d bryanchen-d merged commit 363a525 into main May 7, 2026
30 checks passed
@bryanchen-d bryanchen-d deleted the bryanchen-d/terminal-resize-dispose-race branch May 7, 2026 08:36
@vs-code-engineering vs-code-engineering Bot added this to the 1.120.0 milestone May 7, 2026
bryanchen-d added a commit that referenced this pull request May 7, 2026
Follow-up to #314795. The `isDisposed` early-return at the top of
`_updatePtyDimensions` (plus the matching guards on the resize closures
and `TerminalResizeDebouncer`) already covers all three telemetry
buckets that motivated #314795: `aaa283a2`, `4826565b`, `1e83a096`. The
disposable store flips `isDisposed` to true before disposing children,
so any debounced/idle callback that fires after
`TerminalInstance.dispose()` returns early before reaching
`rawXterm.dimensions`.

The try/catch was added defensively against a hypothetical future race
where xterm.js' `RenderService` is disposed independently of the
`TerminalInstance` lifecycle. That scenario is not represented in the
current telemetry, and swallowing the throw would suppress the signal
we'd need to diagnose it. Prefer to surface such failures via telemetry
and address them at the source — xterm.js' `Terminal.dimensions` is
typed `IRenderDimensions | undefined`, so the getter should honor that
contract rather than throw post-dispose.

Refs #314795
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.

[Unhandled Error] Uncaught TypeError: Cannot read properties of undefined (reading 'dimensions')

3 participants