Skip to content

fix(core): stop running further effects once one destroys the view mid-flush - #65698

Open
arturovt wants to merge 1 commit into
angular:mainfrom
arturovt:fix/effects-null
Open

fix(core): stop running further effects once one destroys the view mid-flush#65698
arturovt wants to merge 1 commit into
angular:mainfrom
arturovt:fix/effects-null

Conversation

@arturovt

@arturovt arturovt commented Nov 27, 2025

Copy link
Copy Markdown
Contributor

When a view has more than one effect scheduled to run, and one of them
destroys the view (e.g. by calling componentRef.destroy()), the
remaining effects in that same flush could still run afterward,
against a view that no longer exists. In some cases this crashed
outright with TypeError: view[EFFECTS] is not iterable.

Here's why: runEffectsInView walks a view's effects in a for...of
loop, wrapped in an outer while loop that re-checks for any effects
that became dirty as a side effect of ones that already ran. When an
effect destroys its view, view[EFFECTS] gets set to null as part
of tearing the view down.

First attempt checked for that inside the for...of loop, before each
effect runs. That covers a sibling effect later in the same pass,
but misses a second case: if the effect that destroys the view also
dirties another effect on that same view in the process (e.g. by
writing a signal the sibling depends on), the outer while loop sees
HasChildViewsToRefresh set and tries to restart — and immediately
crashes re-entering for (const effect of view[EFFECTS]) on a
now-null value, before the in-loop check ever gets a chance to run.

Reproduced that exact crash with a test first: two effects on one
view, the second one writes a signal the first depends on and then
destroys the view in the same call — confirmed it throws before
touching the fix.

Fixed by checking right after effect.run() instead of before it,
covering both cases in one place: the remaining effects in the current
pass, and the loop trying to restart afterward. As soon as one effect
destroys the view, nothing else runs against it again.

This is intentionally narrow in scope. An earlier version of this fix
also tried to guarantee that onCleanup() callbacks still ran even
when registered after an effect destroyed its own view. That's been
dropped — destroying your own view and then continuing to register
more work for it isn't something the framework should have to paper
over. If you need to do both, register onCleanup first, then
destroy.

@pullapprove
pullapprove Bot requested a review from crisbeto November 27, 2025 19:26
@angular-robot angular-robot Bot added the area: core Issues related to the framework runtime label Nov 27, 2025
@ngbot ngbot Bot added this to the Backlog milestone Nov 27, 2025
@JeanMeche

Copy link
Copy Markdown
Member

If this is the same issue as #62822, this is probably not the fix we want.

@arturovt

This comment was marked as outdated.

@JeanMeche

Copy link
Copy Markdown
Member

Chatted offline with Arturo.
TLDR: We probably want to allow an effect to destroy its component but we should still run the effect cleanup function.

@arturovt arturovt changed the title fix(core): guard against view[EFFECTS] being nullified during effect execution fix(core): handle view destruction during effect execution Nov 30, 2025
@JeanMeche

Copy link
Copy Markdown
Member

(can you please resolve the conflict ?)

@JeanMeche JeanMeche added the action: global presubmit The PR is in need of a google3 global presubmit label Dec 8, 2025
Comment thread packages/core/test/render3/reactivity_spec.ts
@JeanMeche

Copy link
Copy Markdown
Member

TGP looks "green" to me

@JeanMeche JeanMeche removed the action: global presubmit The PR is in need of a google3 global presubmit label Dec 8, 2025
@JeanMeche
JeanMeche requested review from atscott and removed request for crisbeto December 8, 2025 19:27
@atscott
atscott requested a review from alxhub December 9, 2025 18:52
@atscott

atscott commented Dec 9, 2025

Copy link
Copy Markdown
Contributor

I think @alxhub should probably review this

@JeanMeche JeanMeche added the action: review The PR is still awaiting reviews from at least one requested reviewer label Jan 8, 2026
@alxhub alxhub self-assigned this Mar 26, 2026
@arturovt
arturovt force-pushed the fix/effects-null branch from 13ff889 to 59d548e Compare May 17, 2026 15:01

@atscott atscott 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.

AGENT: Thanks for working on this.

Please scope this PR strictly to the issue reported in #62822 (TypeError: view[EFFECTS] is not iterable).

  1. runEffectsInView loop guard: Checking view[EFFECTS] === null only at the while loop level is insufficient. If a view has multiple dirty effects and one destroys the view, view[EFFECTS] is set to null mid-iteration, but the for...of loop continues and executes subsequent effects on the destroyed view. The bail-out check must be inside the for loop so execution stops immediately.

  2. Revert the cleanup changes in effect.ts: Destroying a component/view from within its own effect and then continuing execution to register cleanups is an antipattern. If an effect triggers the destruction of its host, it must not continue executing or setting up teardown logic for work on a dead component. Modifying onCleanup to run callbacks synchronously inline upon registration to accommodate this fundamentally breaks the contract and mental model of onCleanup. We will not add framework complexity or workarounds to support this pattern.

Comment thread packages/core/src/render3/reactivity/view_effect_runner.ts Outdated
Comment thread packages/core/src/render3/reactivity/effect.ts Outdated
@arturovt arturovt changed the title fix(core): handle view destruction during effect execution fix(core): stop running further effects once one destroys the view mid-flush Aug 15, 2026
@arturovt
arturovt requested a review from atscott August 15, 2026 09:23

@atscott atscott 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.

AGENT: Thanks for updating this!

Two remaining items to address:

  1. runEffectsInView still crashes if the while loop restarts:
    Moving the check inside the for...of loop protects against subsequent effects in the current iteration, but if tryFlushEffects evaluates to true (e.g. an effect updated a signal that dirtied a child view effect before destroying the view), the while loop re-enters. At that point, JavaScript tries to evaluate for (const effect of view[EFFECTS]) on null and throws TypeError: view[EFFECTS] is not iterable before entering the loop.

    We can fix this by returning immediately after effect.run() if view[EFFECTS] === null (or checking in the while condition). Please also add a test covering this scenario (an effect dirtying another effect while destroying the view).

  2. Remove the documentation / JSDoc additions:
    We generally avoid documenting obscure edge cases in the general guides and public JSDocs as it adds unnecessary complexity to the documentation. Let's remove the changes in adev/src/content/guide/signals/effect.md and packages/core/src/render3/reactivity/effect.ts.

Comment thread packages/core/src/render3/reactivity/view_effect_runner.ts Outdated
Comment thread adev/src/content/guide/signals/effect.md Outdated
@arturovt
arturovt requested a review from atscott August 17, 2026 20:56
Comment thread packages/core/src/render3/reactivity/view_effect_runner.ts
…d-flush

When a view has more than one effect scheduled to run, and one of them
destroys the view (e.g. by calling `componentRef.destroy()`), the
remaining effects in that same flush could still run afterward,
against a view that no longer exists. In some cases this crashed
outright with `TypeError: view[EFFECTS] is not iterable`.

Here's why: `runEffectsInView` walks a view's effects in a `for...of`
loop, wrapped in an outer `while` loop that re-checks for any effects
that became dirty as a side effect of ones that already ran. When an
effect destroys its view, `view[EFFECTS]` gets set to `null` as part
of tearing the view down.

First attempt checked for that inside the `for...of` loop, before each
effect runs. That covers a sibling effect later in the *same* pass,
but misses a second case: if the effect that destroys the view *also*
dirties another effect on that same view in the process (e.g. by
writing a signal the sibling depends on), the outer `while` loop sees
`HasChildViewsToRefresh` set and tries to restart — and immediately
crashes re-entering `for (const effect of view[EFFECTS])` on a
now-null value, before the in-loop check ever gets a chance to run.

Reproduced that exact crash with a test first: two effects on one
view, the second one writes a signal the first depends on and then
destroys the view in the same call — confirmed it throws before
touching the fix.

Fixed by checking right after `effect.run()` instead of before it,
covering both cases in one place: the remaining effects in the current
pass, and the loop trying to restart afterward. As soon as one effect
destroys the view, nothing else runs against it again.

This is intentionally narrow in scope. An earlier version of this fix
also tried to guarantee that `onCleanup()` callbacks still ran even
when registered after an effect destroyed its own view. That's been
dropped — destroying your own view and then continuing to register
more work for it isn't something the framework should have to paper
over. If you need to do both, register `onCleanup` first, then
destroy.
@arturovt
arturovt requested a review from atscott August 17, 2026 22:28
@atscott atscott added action: merge The PR is ready for merge by the caretaker target: patch This PR is targeted for the next patch release and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action: merge The PR is ready for merge by the caretaker area: core Issues related to the framework runtime target: patch This PR is targeted for the next patch release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

"TypeError: view[EFFECTS] is not iterable" when effect causes a component to no longer exist

5 participants