A thermometer reading changes. A display should update, a log should record it, and above 28° an alarm should fire.
What people write first
class Thermometer { private temperature = 21;
set(value: number) { this.temperature = value;
this.display.render(value); // ← now the thermometer this.logger.write(value); // knows about three if (value > 28) this.alarm.trigger(); // unrelated subsystems }}Every new reaction means editing Thermometer. Worse, Thermometer now cannot
be used anywhere those three classes do not exist — you cannot test it without
constructing a logger, and you cannot reuse it in a context that has no display.
The pattern
Invert it. The thermometer keeps a list of things to call, and does not know what any of them are.
type Listener = (temperature: number) => void;
class Thermometer { private listeners = new Set<Listener>(); private temperature = 21;
/** Returns the unsubscribe function — see the gotcha below. */ subscribe(listener: Listener): () => void { this.listeners.add(listener); return () => this.listeners.delete(listener); }
set(value: number) { this.temperature = value; for (const listener of this.listeners) listener(value); }}Detach a subscriber and push the temperature around:
Subject
21°C
The subject has no idea who is listening — it calls notify() and stops caring.
- waiting…
- waiting…
- detached
The three problems
Observer is one of the few catalogue patterns with genuine, well-known hazards.
Iterating a list that is being modified
An observer that unsubscribes itself while the loop is running will corrupt the iteration in most languages:
// Copy first, then notify.for (const listener of this.listeners) listener(value);for (const listener of [...this.listeners]) listener(value);JavaScript’s Set iterator happens to tolerate deletion, but an observer that
adds one during notification will see it called in the same round — probably
not what anyone intended. Copying makes the behaviour explicit either way.
Where it already is
The DOM (addEventListener), Node’s EventEmitter, RxJS, Svelte stores,
React’s useSyncExternalStore, every reactive framework’s dependency tracking,
and every message broker. Observer is less a pattern you implement than one you
recognise.
Which is a reason to use the platform’s version rather than writing your own:
EventTarget is built in, handles the copy-before-iterate problem, and supports
AbortSignal for bulk unsubscription.
const controller = new AbortController();target.addEventListener('reading', onReading, {signal: controller.signal});controller.abort(); // removes every listener registered with this signalPush or pull
The version above pushes the new value into each observer. The alternative is to notify with nothing and let each observer pull what it needs:
for (const listener of this.listeners) listener(); // "something changed"Push is simpler and avoids observers reading a subject that has changed again since. Pull scales better when the subject is large and different observers care about different parts of it — which is exactly the trade a fine-grained reactive system is making when it tracks dependencies per property.