Skip to content

Add ErrorBoundary component with @retryWith support - #2

Draft
megothss wants to merge 22 commits into
mainfrom
error-boundaries
Draft

megothss wants to merge 22 commits into
mainfrom
error-boundaries

Conversation

@megothss

@megothss megothss commented Mar 6, 2026

Copy link
Copy Markdown
Owner

Adds the <ErrorBoundary> component to Ember.js with full error recovery support.

Key features:

  • Catches synchronous render errors (initial render and rerender)
  • Named blocks: <:default> for content, <:error> for fallback UI
  • @retryWith arg for automatic error recovery when a tracked value changes
  • Isolates modifier install errors in transaction commit
  • DEBUG-only console.error logging when errors are caught

Test coverage:

  • Integration tests for initial render, rerender, retry, nested boundaries, sibling isolation, @retryWith with primitives/arrays/objects/undefined

Demo app

@github-actions

github-actions Bot commented Mar 6, 2026

Copy link
Copy Markdown

📊 Package size report   1%↑

File Before (Size / Brotli) After (Size / Brotli)
Total (Includes all files) 17.1 MB / 3.2 MB 1%↑17.3 MB / 1%↑3.2 MB
Tarball size 3.8 MB 1%↑3.9 MB
Show files (28 files)
File Before (Size / Brotli) After (Size / Brotli)
dist/ember-template-compiler.js 765.9 kB / 113.8 kB 0.09%↑766.5 kB / 0.2%↑114 kB
dist/ember-template-compiler.js.map 1.5 MB / 259.7 kB 0.2%↑1.5 MB / 0.3%↑260.4 kB
dist/ember.debug.js 2.1 MB / 320.6 kB 2%↑2.1 MB / 2%↑326.2 kB
dist/ember.debug.js.map 3.4 MB / 632.7 kB 1%↑3.5 MB / 1%↑641.4 kB
dist/ember.prod.js 1.9 MB / 291.3 kB 2%↑1.9 MB / 2%↑296.9 kB
dist/ember.prod.js.map 3.3 MB / 605.4 kB 2%↑3.3 MB / 1%↑614.5 kB
dist/packages/@glimmer/validator/index.js 40.8 kB / 8.8 kB 5%↑42.6 kB / 6%↑9.3 kB
dist/packages/ember-template-compiler/index.js 466.4 kB / 92.1 kB 0.1%↑466.9 kB / 0.2%↑92.3 kB
dist/packages/shared-chunks/api-6Ie8hE3O.js 10.2 kB / 2.2 kB 2%↑10.4 kB / 8%↑2.3 kB
dist/packages/shared-chunks/capabilities-G1QB3ga_.js 32.2 kB / 7.8 kB 0.4%↑32.4 kB / 0.4%↑7.8 kB
dist/packages/shared-chunks/dynamic-DpE3-PQ-.js 118.5 kB / 23.9 kB
dist/packages/shared-chunks/element-builder-BYEA-Hpv.js 11.7 kB / 2.6 kB
dist/packages/shared-chunks/index-BGP1rw3B.js 189 kB / 41 kB 1%↑191.3 kB / 1%↑41.4 kB
dist/packages/shared-chunks/on-BglEnC_e.js 154.3 kB / 31.3 kB
dist/packages/shared-chunks/render-BfzFrO7N.js 38.7 kB / 8.4 kB -6.7%↓36.1 kB / -1.22%↓8.3 kB
types/stable/@ember/-internals/glimmer/index.d.ts 14.1 kB / 3.1 kB 0.7%↑14.2 kB / 0.6%↑3.1 kB
types/stable/@ember/-internals/glimmer/lib/components/error-boundary.d.ts 169 B / 132 B
types/stable/@ember/-internals/glimmer/lib/templates/error-boundary.d.ts 171 B / 116 B
types/stable/@glimmer/interfaces/lib/dom/attributes.d.ts 4.8 kB / 1.2 kB 3%↑5 kB / 4%↑1.3 kB
types/stable/@glimmer/interfaces/lib/managers/internal/component.d.ts 12.1 kB / 2.5 kB 2%↑12.4 kB / 3%↑2.6 kB
types/stable/@glimmer/interfaces/lib/runtime/debug-render-tree.d.ts 1.4 kB / 407 B 19%↑1.6 kB / 20%↑490 B
types/stable/@glimmer/runtime/index.d.ts 2.6 kB / 753 B 4%↑2.8 kB / 3%↑774 B
types/stable/@glimmer/runtime/lib/component/error-boundary.d.ts 1.4 kB / 438 B
types/stable/@glimmer/runtime/lib/vm/append.d.ts 11.9 kB / 2.2 kB 8%↑12.9 kB / 12%↑2.5 kB
types/stable/@glimmer/runtime/lib/vm/element-builder.d.ts 6.3 kB / 1.3 kB 25%↑7.9 kB / 38%↑1.8 kB
types/stable/@glimmer/runtime/lib/vm/update.d.ts 3.1 kB / 775 B 87%↑5.9 kB / 96%↑1.5 kB
types/stable/@glimmer/validator/lib/tracking.d.ts 925 B / 336 B 108%↑1.9 kB / 96%↑657 B
types/stable/index.d.ts 43.3 kB / 4 kB 0.6%↑43.6 kB / 0.5%↑4.1 kB

🤖 This report was automatically generated by pkg-size-action

megothss added 20 commits March 15, 2026 14:46
…ntegration tests

Fixes the updating opcode integration so ErrorBoundaryOpcode is properly
placed in the parent VM's updating list (not pushed onto the stack where
it gets lost). Adds JS exception handling in UpdatingVM._execute with
tracking frame rollback, executeGuarded() for sub-VM isolation, debug
render tree rollback, and backflow assertion bypass in setError().
Includes 7 integration tests covering initial render, rerender, retry,
nested boundaries, and missing error block.
Cache DOM boundaries before child opcodes execute so error recovery can
clean up correctly even when inner TryOpcodes corrupt the bounds tree.
Add beginBlock nextSibling parameter for proper insertion positioning.
Add comprehensive integration tests for error boundary edge cases.
…OM refs

Replace try-catch in evaluate() with unconditional snapshot + debug assertion,
since bounds are always initialized. Null out cached DOM references after error
recovery to avoid retaining detached nodes.
…ge.json

The ember-template-compiler/index.js exposed dependency mapping was
unintentionally removed in an earlier commit.
Replace vm.execute() with vm.executeGuarded() in nested VM contexts to
prevent resetTracking() from destroying parent tracking frames. In DEBUG
mode, AppendingVM.execute() calls resetTracking() on error, which wipes
ALL tracking state including the parent UpdatingVM's frames. This made
ErrorBoundary recovery impossible and caused "attempted to close a
tracking frame" crashes.

Fixes:
- Backtracking assertions on tracked mutation after error recovery
- Double-trigger tracking frame corruption (error → error → retry)
- Error-in-error-block rendering failures bubbling incorrectly
- ListBlockOpcode.insertItem tracking corruption when new items throw

Changes:
- TryOpcode.handleException(): use executeGuarded() so errors propagate
  cleanly to the UpdatingVM's catch handler without wiping tracking state
- ErrorBoundaryOpcode.handleException(): restore tracking before re-render,
  use executeGuarded(), add robust DOM cleanup with lastPreviousSibling
  fallback for when inner TryOpcodes detach cached nodes via bounds.reset()
- ErrorBoundaryOpcode.handleError(): restore tracking and consumed tags
  before transitioning to error state
- ErrorBoundaryOpcode.transitionToError(): use executeGuarded(), add
  previousSibling-based DOM cleanup fallback, roll back debug render tree
- ListBlockOpcode.insertItem(): use executeGuarded()
- Add resetConsumedTags() to debug.ts to discard stale CONSUMED_TAGS
  entries after popping tracking frames during error recovery

Adjusts assertion count in "readable error stack" test (7→6): the single
outer console.error from UpdatingVM.execute() now captures full tracking
info since the inner call no longer prematurely wipes it.

Adds 5 regression tests covering all fix scenarios.
- Add handleError() to ExceptionHandler interface, removing unsafe `any`
  casts in UpdatingVMFrame.handleError()
- Cast tagFor() results to UpdatableTag in ErrorBoundaryState.setError()
  to satisfy dirtyTag() parameter type
- Cast array access in restoreTrackingTo() to fix Tracker | undefined
  not assignable to Tracker | null
- Extract inline decorated class in test to fix "decorators are not
  valid here" TS error
- Change defineSimpleHelper param from boolean to unknown to match
  expected signature
- Remove unused imports (destroyChildren, clear) from component.ts
- Run prettier on all modified files
ErrorBoundary should only be importable from @ember/component, matching
the pattern of Input and Textarea. The value re-export was pulling 51
source files into type-tests compilation, causing 349 type errors.
The console.error logging is guarded by DEBUG in the source, so these
tests can't pass in production builds.
A single modifier throwing during transaction.commit() previously aborted
the entire loop, leaving all subsequently-scheduled modifiers uninstalled.
Wrap each install/update in try-catch, collect the first error, and
re-throw after all modifiers have been processed.
The DEBUG-only console.error tests bail early in production builds
without ever setting this.component, causing the conditional
runDestroy to be skipped and leaking Application namespaces.
…oundary

The guarded opcode handler (VM_INVOKE_COMPONENT_LAYOUT_GUARDED_OP) created
a sub-VM tree builder with nextSibling=null, causing content to be appended
at the end of the parent element instead of at the parent VM's cursor
position. This inverted the bounds delegation chain (firstNode after
lastNode in DOM order), crashing clear() during route transitions.

Fix: capture the parent tree builder's nextSibling before pushing the
ResettableBlock and pass it to the sub-VM's tree builder.

Also improves ErrorBoundaryOpcode.handleException to use direct DOM
cleanup with cached node references instead of walking the delegation
chain, and balances the parent tree builder's block stack with popBlock().
Track error boundary depth on the UpdatingVM so _execute() only pays
the try-catch cost when inside an ErrorBoundary frame. Also reverts
ListBlockOpcode.insertItem() to use execute() instead of
executeGuarded().
…code

- Make handleCaughtError required on ExceptionHandler interface
- Add unreachable() stubs in TryOpcode and RenderResultImpl
- Add clarifying comments for null end marker and resetPartial
- Add dropBlocks() to TreeBuilder: pops blocks without finalize() during
  error cleanup, preventing placeholder comment leaks into remote elements
- Destroy sub-VM's destroyable root in executeGuarded catch path so
  RemoteBlock destructors fire and clear orphaned remote DOM
- Guard RemoteBlock destructor against uninitialized bounds (error thrown
  before any content rendered into the remote element)
- Add integration tests for {{#in-element}} inside ErrorBoundary (initial
  render error + rerender error)
The previous commit introduced dropBlocks() for ErrorBoundary cleanup, but
used it in executeGuarded which is also called by TryOpcode. TryOpcode
needs popBlock/finalize to insert <!----> placeholders. Split into two
methods: executeGuarded (popBlock) for TryOpcode, executeErrorBoundary
(dropBlocks + destroy) for ErrorBoundary.
The PERF commit changed ListBlockOpcode.insertItem() from executeGuarded()
to execute(), but executeGuarded is needed to clean up blocks/tracking
when a list item throws inside an ErrorBoundary. Also fixes eslint
non-null assertion and prettier formatting.
# Conflicts:
#	packages/@ember/component/index.ts
#	packages/@glimmer/runtime/lib/compiled/opcodes/component.ts
#	packages/@glimmer/runtime/lib/vm/append.ts
#	packages/@glimmer/runtime/lib/vm/update.ts
Upstream deleted defComponent and defineComponent from
internal-test-helpers when it unified the test infrastructure on
precompileTemplate. Rewrite all 72 call sites to the replacement idiom:

  setComponentTemplate(
    precompileTemplate(src, { strictMode: true, scope: () => ({ ... }) }),
    templateOnly()
  )

Templates that passed no scope stay non-strict, matching what the old
helper did with a null scope.
@github-actions github-actions Bot mentioned this pull request Sep 9, 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