Skip to content

Isolate errors thrown by one element during a shared-timer tick - #399

Open
mattcosta7 wants to merge 2 commits into
fix-retain-disconnected-elementsfrom
isolate-tick-errors-on-leak-fix
Open

mattcosta7 wants to merge 2 commits into
fix-retain-disconnected-elementsfrom
isolate-tick-errors-on-leak-fix

Conversation

@mattcosta7

Copy link
Copy Markdown
Member

Stacked on #396. Supersedes #371 and #398.

What this fixes

All <relative-time> elements share one timer. Each tick loops over the elements, then schedules the next tick. On main, if anything throws while one element is handled, the loop is abandoned. The elements after it aren't updated and the next tick is never scheduled. One broken element, for example one an hour or more old with an invalid time-zone, freezes every timestamp on the page.

The change

In dateObserver.update(), each element is handled in its own try/catch: its connected check, getUnitFactor and update().

  • If getUnitFactor throws, the element doesn't count toward the next tick's delay and isn't rendered.
  • If update() throws, the element's delay still counts, so it's retried on the next tick.
  • The error is rethrown from setTimeout(() => { throw error }). So it still reaches the page as an uncaught error, asynchronously and exactly once per throw. I used setTimeout instead of reportError, which older browsers and non-browser hosts lack; the observer already depends only on setTimeout.
  • An element that fails is skipped if the tick reaches it again, for example after it moved itself in the document and rejoined the observer's set. Otherwise it would throw on every visit and the tick would never finish. Elements added during a tick are still visited, as before.

Errors thrown from connectedCallback and from the attribute-change microtask behave as before.

One behavior changes: a permanently broken element now reports its error on every tick. Before, the first error stopped the timer for the whole page.

Why #371 and #398 are superseded

Tests

The new suite errors thrown during a tick in test/relative-time.js covers these cases:

  • elements whose update(), isConnected getter or date getter throws
  • an element over an hour old with an invalid time-zone
  • an element that moves itself and throws, which is handled once per tick
  • a failing element, reported once per tick and retried on the next
  • several failing elements in one tick, each reported
  • the error reaching window as an uncaught error event through the real setTimeout
  • a control showing that elements that don't throw report nothing

The setTimeout stub tells the observer's timer, which has a delay, apart from the rethrow, which has none, so rethrows are captured rather than dropped.

With the source change reverted and the new tests kept, the 8 isolation tests fail and the control passes.

Measured in Chromium with 500 elements a few hours old and one fresh element ticking every second, the per-element try doesn't change tick cost: about 20.5 ms of busy time per 8 s both with and without it.

Command (Node 22) Result
npm test (build, lint, web-test-runner) 679 passed, 0 failed
npx prettier --check src test passes
npx web-test-runner --files test/relative-time.js with src from #396 404 passed, 8 failed

All relative-time elements share one timer. If anything threw while the
tick handled one element, including its isConnected check, getUnitFactor
(for example an invalid time-zone on an element an hour or more old) or
update(), the loop was abandoned: later elements weren't updated and the
next tick was never scheduled, so every timestamp on the page froze.

Each element is now handled in its own try/catch, and the error is
rethrown from a task of its own so it is still reported, uncaught,
exactly once. An element that fails is skipped if the tick reaches it
again, as when it moved itself and rejoined the observer's set, so the
tick always finishes. Supersedes #371.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@mattcosta7
mattcosta7 marked this pull request as ready for review November 9, 2026 00:18
@mattcosta7
mattcosta7 requested a review from a team as a code owner November 9, 2026 00:18
Copilot AI auto review requested due to automatic review settings November 9, 2026 00:18
@mattcosta7
mattcosta7 added this pull request to stack #400 November 9, 2026 00:18

Copilot AI left a comment

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.

🟢 Approval recommended

No active review issues are recorded for the changes isolating per-element failures in the shared timer.

No open issues 🎉

What changed in this PR

This PR isolates per-element failures in the shared <relative-time> timer, building on #396 so one broken element no longer prevents other timestamps from updating or stops future ticks.

Changes:

  • Guards connection checks, scheduling calculations, and rendering, while reporting caught errors asynchronously.
  • Prevents failed elements that rejoin the observer from being processed repeatedly in the same tick.
  • Adds regression tests for failure isolation, retries, timer scheduling, and uncaught error reporting.
File Description
test/​relative-time.js Adds shared-timer error-isolation and error-reporting tests.
src/​relative-time-element.ts Catches per-element failures and skips failed elements on repeat visits within a tick.

Review effort: Balanced (auto)

@mattcosta7 mattcosta7 self-assigned this Nov 9, 2026
@mattcosta7
mattcosta7 requested a review from jonrohan November 9, 2026 19:08

This branch has not been deployed

No deployments
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.

2 participants