Angular NG0600: Writing to Signals Is Not Allowed in a computed — Fix

Angular NG0600 'Writing to signals is not allowed in a computed' means a read-only context wrote state. See the exact causes, v17–v22 behaviour, and the real fix.

angular angular-errors ng0600 signals computed effect linkedSignal untracked
Bharath
Reading Progress

On This Page

Angular NG0600: Writing to Signals Is Not Allowed in a computed — Fix

The Error

ERROR RuntimeError: NG0600: Writing to signals is not allowed in a `computed`.
    at throwInvalidWriteToSignalErrorFn (core.mjs:...)
    at signalSetFn (core.mjs:...)
    at WritableSignalImpl.set (core.mjs:...)
    at CartComponent.<computed> (cart.component.ts:14:18)
    at producerRecomputeValue (core.mjs:...)
    at producerUpdateValueVersion (core.mjs:...)
    at computedGet (core.mjs:...)
    at CartComponent_Template (cart.component.html:3:12)
    at executeTemplate (core.mjs:...)
    at refreshView (core.mjs:...)

Docs: https://angular.dev/errors/NG0600. Frame names and line numbers vary by version; the identifying frames are signalSetFn/set called under computedGet or executeTemplate.

Wording differs by context and version:

  • v17–v18: the message was "Writing to signals is not allowed in a computed or an effect by default. Use allowSignalWrites in the CreateEffectOptions to enable this inside effects." That is where the effect part of the old advice comes from.
  • v19+: per the Angular team's post on effect() changes, allowSignalWrites was removed in v19 and effects may write signals by default. Per angular.dev/errors/NG0600, the error now covers writes during a computed, during a linkedSignal computation, and during template rendering. Check the exact string in your installed @angular/core (node_modules/@angular/core/fesm2022/*.mjs, search for NG0600), because the suffix changes by context.
  • Production: the dev message is replaced by the bare code and a link to the error page; the throw itself still happens, since it protects graph consistency, not just a dev assertion.

How to Reproduce It

npx @angular/cli@22 new ng0600-repro --defaults
cd ng0600-repro
ng version        # Node must satisfy the CLI's supported range

src/app/app.ts (v22 zoneless default; no provideZonelessChangeDetection() call needed in generated apps since v21):

import { Component, computed, signal } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <button (click)="price.set(price() + 5)">+5</button>
    <p>Price: {{ price() }} | Tax: {{ tax() }} | Total: {{ total() }}</p>
  `,
})
export class App {
  price = signal(100);
  tax = signal(0);

  // BUG: a computed that writes
  total = computed(() => {
    const t = this.price() * 0.2;
    this.tax.set(t);            // NG0600 here
    return this.price() + t;
  });
}
ng serve

The first render evaluates total() from the template; the set inside the computed throws and the view fails to render. The same error appears when a template calls a method that writes ({{ label() }} where label() increments a counter signal).

Triggers: any environment; independent of zone vs zoneless, SSR vs CSR, dev vs production.

Version Behaviour Matrix

Releaseset() in computedset() in effectNotes
v17NG0600NG0600 unless allowSignalWrites: trueSignals developer preview; effect experimental-ish
v18NG0600sameMessage mentions both computed and effect
v19NG0600Allowed; allowSignalWrites removedlinkedSignal and resource() arrive experimental
v20NG0600AllowedCore signal APIs stable
v21NG0600AllowedZoneless default for new apps; Signal Forms experimental
v22NG0600Allowedresource() family stable; verify against the v22 release post

Ecosystem: NgRx SignalStore and toSignal follow the same rule; anything you call from a computed is "inside" it, including service methods. v17/v18 are out of support as of this run (support is ~6 months active + 12 months LTS; check angular.dev/reference/releases).

Why It Happens — Surface Level

A computed is supposed to be a pure function of other signals. Writing to a signal from within it changes state while Angular is in the middle of deciding what the current state is. Angular refuses: it throws NG0600 at the set/update call. Templates are treated the same way, because reading {{ x() }} while rendering must not mutate anything that has already been rendered.

The usual real-world causes: deriving a second value and "caching" it in a signal, calling a service method from a computed that secretly updates a store, a template-bound method that bumps a counter, or a linkedSignal computation that sets a sibling.

Why It Happens — Under the Hood

Signals form a graph of producers and consumers. Each producer has a version number; a computed is both a consumer (of the signals it reads) and a producer (to whoever reads it). Reads are recorded by setting an "active consumer" for the duration of the computation. computed is lazy: it recomputes only when read and only if a dependency's version changed, then caches the result.

The write guard hooks into that same mechanism. signalSetFn checks whether the current active consumer forbids writes (consumerAllowSignalWrites is false for computeds and for the template consumer). If so, it calls the throw function that produces NG0600. Why forbid it?

  1. Glitch-freedom. Propagation marks dependents dirty and recomputes lazily on read. A write mid-recompute would dirty nodes that may already have been read in this pass, so some consumers would see the pre-write value and others the post-write value.
  2. Termination. A computed that writes a signal it (transitively) reads would re-dirty itself forever.
  3. Laziness. A computed may run zero, one or many times depending on who reads it. A side effect inside it would be nondeterministic.

Effects are different: they are scheduled consumers that run after the graph settles, so a write schedules another run rather than corrupting an in-flight read. That's why v19 could drop the flag. The risk moved from "forbidden" to "your problem": an effect that writes a signal it also reads loops.

untracked() suppresses the error because it clears the active consumer for the callback, so the guard sees no consumer. It also means any signal read inside is not a dependency, which is how stale values appear.

The Fix

Best: make the derived value a computed, not a written signal.

 export class App {
   price = signal(100);
-  tax = signal(0);
-
-  total = computed(() => {
-    const t = this.price() * 0.2;
-    this.tax.set(t);
-    return this.price() + t;
-  });
+  tax = computed(() => this.price() * 0.2);
+  total = computed(() => this.price() + this.tax());
 }

If the derived value must also be user-writable (e.g. a selection that resets when the options change), use linkedSignal:

import { linkedSignal, signal } from '@angular/core';

options = signal(['a', 'b', 'c']);
selected = linkedSignal<string[], string>({
  source: this.options,
  computation: (opts, prev) => (prev && opts.includes(prev.value) ? prev.value : opts[0]),
});

Do not write other signals inside the computation; that raises NG0600 too.

If it is a genuine side effect (logging, syncing to localStorage, calling an imperative API), use effect (v19+ may write signals, but prefer not to):

constructor() {
  effect(() => localStorage.setItem('price', String(this.price())));
}

Template-bound method that writes: move the write into an event handler, ngOnInit, or an effect, and bind to a signal or computed instead.

Quick unblock, with cost: untracked(() => this.tax.set(t)). This silences the error but keeps the problem: the computed now has a hidden side effect that runs a nondeterministic number of times, and t isn't tracked if you read signals inside. Use only when the write is intentional and cannot be relocated, per the docs. Also avoid setTimeout(() => this.tax.set(t)) — it hides the symptom and causes extra render passes.

Best Practices & The Better Design

  • Model state as a small set of writable signals (the sources of truth) and everything else as computed.
  • Keep computed bodies pure: no service calls that mutate, no Date.now()-driven writes, no logging with side effects.
  • Prefer linkedSignal over "effect that resets a signal when another changes".
  • For async derived data use resource()/httpResource() (stable in v22) rather than writing loading/data signals by hand.
  • If a template calls a method, make it read-only, or replace it with a computed.

Right-way component (Angular 22, zoneless default):

import { Component, computed, signal } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <button (click)="bump()">+5</button>
    <p>Price {{ price() }} / Tax {{ tax() }} / Total {{ total() }}</p>
  `,
})
export class App {
  readonly price = signal(100);
  readonly tax = computed(() => this.price() * 0.2);
  readonly total = computed(() => this.price() + this.tax());
  bump() { this.price.update(p => p + 5); }
}

How to Prevent It Long-Term

  • Code review rule: a computed or linkedSignal body contains no .set(, .update(, or calls to methods that might.
  • Lint with angular-eslint and @typescript-eslint; add a custom no-restricted-syntax rule for .set( inside computed( callbacks if the team hits this often.
  • Run ng build --configuration production and component tests (Vitest + TestBed) in CI; NG0600 is deterministic, so a render test catches it.
  • Make "is this derivable?" part of design review before adding a signal.
  • Use Angular DevTools' signal graph (v22) to see which node is writing and what depends on it.
  • Follow the Update Guide when upgrading across v18 → v19: code that used allowSignalWrites: true can drop it; code relying on effects to sync state is the usual candidate for computed/linkedSignal.

Related articles: effect loops and injection context (NG0203); zoneless staleness; toSignal initial values.

Key Takeaways

  • NG0600 means a read-only reactive context (computed, linkedSignal computation, template) tried to write a signal.
  • v19 removed allowSignalWrites; effects can write now, but writing in effects is usually a sign you want computed or linkedSignal.
  • untracked() silences it and keeps the bug; relocate the write instead.
  • Derive, don't sync: one writable source, many computeds.
angularangular-errorsng0600signalscomputedeffectlinkedSignaluntracked

Engineer and writer behind CODELZ. I read the stack trace, reproduce the bug, and explain why it happens, not just how to silence it. Plus honest reviews of the books and tools worth your time.

Comments