Skip to content
This repository was archived by the owner on Jan 6, 2026. It is now read-only.
This repository was archived by the owner on Jan 6, 2026. It is now read-only.

Incorrect(?) rerender assertion with auto-tracking of modifiers #1762

Description

@simonihmig

When working on upgrading Ember from 5.4 to 5.12, I was hitting a few instances of (incredibly hard to debug and fix) rerender assertions

You attempted to update `<trackedProperty>` on `<ClassName>`, but it had already been used previously in the same computation.  Attempting to update a value after using it in a computation can cause logical errors, infinite revalidation bugs, and performance issues, and is not supported. 

Some might have been legit, but in some cases I couldn't really see any kind of anti-patterns in use (other than maybe using class-based ember-resources, more on that later). However, in some cases (of mainly trial-and-erroring on ways to silence that assertion) I was seeing just removing or refactoring a modifier (that consumed tracked state) to kinda unexpectedly help. But given the hard to see through graph of tracked state and their dependencies (many interconnected resources), I would rather assume something on our side is odd. Luckily I was able to get a reproduction up that is suggesting otherwise, like I am getting the impression that something might be wrong in Glimmer actually.

Reproduction

Some of the ingredients that seem to be necessary to get this to reproduce:

  • use of class-based resources (we are still on an old ember-resources version, but the reproduction uses the latest, with ember-modify-based-class-resource). The thing here is that they have @tracked state, which invalidates (and so gets written to) when reactive dependencies change. When reading their state after invalidation, the key is to have the modify hook run before the read, so the write of tracked state happens before the read.
  • use of a modifier that reads the resource's state (directly or as derived state), so somehow is depending on that state
  • nested components with state being passed in NamedArgsProxy seems to be relevant as well, not entirely sure though

So to explain the reproduction:

  • when clicking the "+1" button, a tracked property is increased. A resource will add +1 to add. That value is passed to a sub component to render it
  • When that reactive state is read by a modifier here first(!), it will hit the assertion.
  • It will not hit the assertion, if you
    • remove the modifier
    • make the modifier do something else or nothing, but not read the state
    • read the state first in a different place in the template, before the modifier. Like uncommenting this line will make the assertion disappear! 🤯

Debugging details

When I was debugging my real app, I was seeing something happen inside GlimmerVM code that maybe is able explain it, or at least provide some leads. I did some hackery inside DevTools, like having a global WeakMap that mapped tags to basically debug info like for which place in code that tag was used (obj + key), and using that inside all consume and dirty tags related code to get some understanding of what is happening. In a getter of a resource, which was the only way to access the otherwise private @tracked state of a resource (the one trigger the assertion for that prop), I was able to see that it was actually not read before the write, at least not in the some computation cycle. When digging into when that tracked state was consumed (consumeTag) without apparently a real read, I could see this happening inside modifier opcode handlers, like here.

That tag had subtags, I believe those are created by calls to track(). Using that debugging hacks, I could see that inside those (nested) subtags, there was also that tracked property of my offending resource. And calling consumeTag() on the tag would also consume all its subtags, including the resource state. But again, the getter was not actually invoked, neither the @tracked decorator getter itself, the tracked state was actually not read, it was only marked as "consumed", ahead of time.

Now when that same tracked state (or derived state based on it) is getting actually read (like {{@value}} in a template), the implementation of class-based resources will correctly understand that the previously cached resource has been invalidated, it will run the modify hook, which will mutate the tracked state. No problem if only consumeTag was not called on it before, but now 💥

I think that also explains why adding {{log @value}} before the modifier would "fix" this, as this has the correct timing: before reading and consuming the tracked state, the modify hook will run, the tracked state is being updated before anyone has marked it as consumed, and only then will the (new!) value get read and marked as consumed. No mutations afterwards, hence no assertion.

Conclusions

I know there are some reservations about class-based resources and maybe some implementation details of resources in general. I was not able to get this reproduced with function-based resources, but that seems understandable given that they don't mutate tracked state on invalidation, but the whole resource gets destroyed and recreated. But given that things work perfectly fine unless modifiers come into play, I would like to believe there is a real issue here that needs fixed.

/cc @mansona @NullVoxPopuli

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions