sveltejs / svelte · Issue No. 18592
<script>
class Q {
#p = $state.raw(null);
get() {
this.#p ??= fetch('/x');
return this.#p;
}
}
</script>
compiles (5.56.6) to
get() {
$.set(this.#p, $.get(this.#p) ?? fetch('/x'));
return $.get(this.#p);
}
Per spec, a ??= b performs no assignment when a is non-nullish. The compiled output assigns on every evaluation, writing the read value back into the source. ||= and &&= compile the same way.
Usually this is invisible because the value is unchanged. It becomes observable in two ways. Reading through a getter that uses ??= inside a $derived throws state_unsafe_mutation even though no mutation should occur (see reproduction). And when the read happens while a batch is pending, the assignment re-commits the pre-batch value into the source, erasing the write the batch had applied. That second case is the mechanism behind sveltejs/kit#16444, where #promise ??= this.#run() on a $state.raw field permanently undid a reset() applied during navigation; the workaround on the kit side is an explicit if (sveltejs/kit#16546).
#17906 made the RHS lazy but left the assignment unconditional. Expected output is something like
if ($.get(this.#p) == null) $.set(this.#p, fetch('/x'));
The playground shows state_unsafe_mutation on load; the JS output tab shows the unconditional $.set. Per spec the derived is legal: the field is non-nullish, so ??= never assigns.
svelte 5.56.6
annoyance
Relay reads this issue against the repository's contribution signals: the files it is likely to touch, how the maintainers triage work this size, and what the first contribution would exercise.
The full analysis for this issue is still being assembled. Until then, the description above and the thread on GitHub are the most reliable context.