[SPIKE] {{#try}}/{{else catch}} template error boundaries - #21548
Draft
NullVoxPopuli-ai-agent wants to merge 1 commit into
Draft
[SPIKE] {{#try}}/{{else catch}} template error boundaries#21548NullVoxPopuli-ai-agent wants to merge 1 commit into
NullVoxPopuli-ai-agent wants to merge 1 commit into
Conversation
Adds a `try` block keyword that catches JavaScript errors thrown while
rendering its block, both during initial render and during updates:
{{#try}}
{{this.mightThrow}}
{{else catch as |error|}}
caught: {{error.message}}
{{/try}}
The syntax comes free from handlebars' chained-else grammar: `{{else
catch as |error|}}` parses as an inverse block invoking `catch` with
block params, and the `try` keyword unwraps it during normalization. A
plain `{{else}}` also works as a catch branch that ignores the error.
The region compiles like other Replayable blocks (one resettable block
+ TryOpcode-style closure), with three new opcodes:
- `EnterTry` creates a `TryErrorOpcode`, a `TryOpcode` marked as an
error boundary.
- `PushTryFrame` snapshots the machine and syscall registers, the six
VM stacks, the tree builder's cursors/blocks and DOM position, and
the open tracking-frame depth, and records the CATCH label.
- `PopTryFrame` disarms the handler when the try branch completes.
If appending the try branch throws, the append loop rolls all of that
state back, destroys the partial content's destroyables, removes its
DOM (by real node positions, since partially-appended inner blocks can
be empty and their bounds unqueryable), pushes a reference to the
caught error, and resumes at CATCH. Updating opcodes that throw unwind
the UpdatingVM's frame stack to the nearest boundary, which re-renders
the region from scratch; the append-time handler renders the catch
branch if the error persists.
Caught regions also retry automatically: `track()` now forwards a
failed computation's combined tag to its parent tracker (a throw
otherwise discards it in the `finally`), and the try frame holds a
wrapper tracking frame open so those dependencies have somewhere to
land. The unwind stores them on the boundary, which revalidates them
on each update pass and re-attempts the try branch when one changes.
Known limitations: modifiers scheduled on discarded elements still
install; errors thrown inside the catch branch propagate; and an
update-time error outside any cache group may not self-schedule the
retry render.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Prototype of a
{{#try}}block keyword that acts as a template error boundary: JavaScript errors thrown while rendering the block — during initial render or during updates — render a catch branch instead of wedging the renderer.No parser changes needed: handlebars' chained-else grammar already parses
{{else catch as |error|}}as an inverse invokingcatchwith block params, closed by{{/try}}. Thetrykeyword unwraps it during normalization. A plain{{else}}works as a catch branch that ignores the error, and a standalone{{#catch}}is a syntax error.How it works
The region compiles like the other
Replayableconstructs — a single resettable block whoseTryOpcode-style closure can re-render it from scratch — plus three new opcodes:EnterTrycreates aTryErrorOpcode, aTryOpcodemarked as an error boundary.PushTryFramesnapshots the machine/syscall registers, the six VM stacks, the tree builder's open cursors/blocks and the region's DOM position, and the open tracking-frame depth, and records the CATCH address. It sits afterEnterTry, so closure re-renders (which resume at the pc captured byEnterTry) re-arm the handler.PopTryFramedisarms the handler when the try branch completes.Append-time errors: the execute loop catches, rolls every piece of snapshotted state back, destroys the partial content's destroyables, removes its DOM, pushes a reference to the caught error, and resumes at CATCH. DOM removal uses real node positions captured in the frame rather than block bounds — partially-appended content contains inner blocks that are still empty, whose bounds can't be queried.
Update-time errors:
UpdatingVMcatches a throwing updating opcode, unwinds its frame stack to the nearestTryErrorOpcode, and re-renders that region from scratch (the same recovery path structural invalidation already uses). If the error persists, the append-time handler renders the catch branch with the fresh error.Automatic retry: the unwind captures everything the failed branch consumed and stores it on the boundary, which revalidates those dependencies on each update pass and re-attempts the try branch when one changes. Two supporting changes make that possible:
track()now forwards a failed computation's combined tag to its parent tracker (a throw otherwise discards it in thefinally), and the try frame holds a wrapper tracking frame open across the try branch so those tags have somewhere to land (PopTryFramecloses it transparently on the happy path).Testing
test/keywords/try-test.ts): initial-render catch, update-time catch, recovery on dependency change, DOM rollback mid-element, nested boundaries, plain-else catch, no-catch swallow, and errors outside any boundary still propagating.Known limitations / open questions
onerror? Retry policy,{{catch}}ergonomics, and interaction with SSR/rehydration are RFC territory — this spike is meant to show the VM machinery is tractable.🤖 Generated with Claude Code