Skip to content

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

Closed
mattcosta7 wants to merge 8 commits into
perf-skip-unchanged-ticksfrom
isolate-tick-errors
Closed

mattcosta7 wants to merge 8 commits into
perf-skip-unchanged-ticksfrom
isolate-tick-errors

Conversation

@mattcosta7

@mattcosta7 mattcosta7 commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

Stacked on #397 (which is stacked on #396). Review only this branch's own commits (d1745dd, c35c916, 19b06cd; the merge commits bring in #397's latest fixes); the base of this PR should be perf-skip-unchanged-ticks.

Supersedes #371.

What this fixes

All <relative-time> elements share one timer. Its tick loops over the elements and, once the loop finishes, schedules the next tick. On main, if anything throws while one element is handled, the loop is abandoned: the elements after it are not updated and the next tick is never scheduled. One bad element therefore freezes every timestamp on the page until something else re-observes an element.

Why #371 is superseded

#371 wrapped only the update() call in try/catch. Ship-check found it incomplete:

The change

In dateObserver.update(), each element's connected check, getUnitFactor and re-render now run in their own try/catch, so even a throwing isConnected getter can't abort the tick:

  • If getUnitFactor throws, the element is skipped for scheduling (it cannot poison nearestDistance) and is not rendered, so its error is reported once.
  • If the render throws, the element's factor still counts, so it is 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. setTimeout is used instead of reportError because reportError is missing from older browsers and non-browser hosts, and the rest of the observer already relies only on setTimeout.
  • Lookups left recording by a failed render are discarded, as the start of each tick already does.
  • 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.

Errors thrown from connectedCallback and the attribute-change microtask are not changed.

Tests

New suite errors thrown during a tick in test/relative-time.js. The setTimeout stub tells the observer's timer (which has a delay) from the rethrow (which has none), so the rethrow is captured, not dropped:

  • an element whose update throws does not stop others updating, and the timer reschedules
  • an element whose date getter throws does not stop others or rescheduling
  • an element over an hour old with an invalid time-zone does not stop others or rescheduling
  • a failing element is reported once per tick and retried on the next
  • every failing element in one tick is reported
  • the error reaches window as an uncaught error event (real setTimeout for the rethrow)
  • elements that do not throw report nothing

All of these except the last (a control) fail on perf-skip-unchanged-ticks without the source change.

Commands run (Node 22):

$ npm test
Chromium: 7/7 test files | 725 passed, 0 failed
$ npx prettier --check src test
All matched files use Prettier code style!

With src/relative-time-element.ts restored to perf-skip-unchanged-ticks and the new tests kept:

$ npx web-test-runner --files test/relative-time.js
Chromium: 1/1 test files | 432 passed, 6 failed

The 6 failures are the first 6 tests listed above.

mattcosta7 and others added 8 commits November 8, 2026 12:22
A throw while updating one element aborted the whole tick: the elements
after it were not updated and the timer was never rescheduled, so every
timestamp on the page stopped updating.

Each element's `getUnitFactor` and re-render now run in their own
try/catch. A failing element is skipped for scheduling when its
`getUnitFactor` threw, and its error is rethrown from a zero-delay
`setTimeout`, so it still reaches the page as an uncaught error exactly
once per throw. `setTimeout` is used rather than `reportError`, which
older browsers and non-browser hosts lack and which the shared timer
does not otherwise need. Lookups left recording by the failed render are
discarded.

This covers errors thrown by `getUnitFactor` as well as by the render
(a throwing `date` getter, or an element over an hour old whose
`time-zone` is invalid and so fails in `msUntilNextDay`). Errors in
`connectedCallback` and the attribute-change microtask are unchanged.

Supersedes #371, which wrapped only `update()`.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…g part way

An element whose isConnected getter throws still aborted the tick, because
the check ran before the per-element try. It now runs inside it. A new
test fails a render after it has started recording formatter lookups and
checks that later elements still skip or render as before.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Patching RelativeTimeElement.prototype.update now turns skipping off, so the
test counts renders through Element.prototype.hasAttribute instead.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
# Conflicts:
#	src/relative-time-element.ts
An element whose update threw after moving itself in the document was
added back to the end of the observer's Set and visited again, throwing
every time, so the tick never finished. An element that fails is now
skipped if the tick reaches it again. Elements added during a tick are
still visited, as before.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
# Conflicts:
#	src/relative-time-element.ts
@mattcosta7

Copy link
Copy Markdown
Member Author

Superseded by #399, which makes the same fix directly on #396 instead of on #397 (parked).

@mattcosta7 mattcosta7 closed this Nov 8, 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.

1 participant