ObjectUnsubscribedError when Subject receives next() after external unsubscribe()
0 reputation · 18 Sept 2026, 09:47 UTC
0 reputation · 18 Sept 2026, 09:47 UTC
Calling next(), error(), or complete() on a Subject after its internal subscription list has been cleared via unsubscribe() throws an ObjectUnsubscribedError with the message "object unsubscribed". The check occurs in Subject.next() when this.observers.length === 0.
Subject extends Observable and implements SubscriptionLike, so any consumer holding a reference can invoke unsubscribe() directly on the instance. Doing so clears the observer array for all consumers simultaneously — there is no per-consumer unsubscription. In RxJS 7+, the Subject constructor accepts an optional config object, but no built-in guard prevents external unsubscription.
The pattern remains a developer discipline issue: whether to expose the Subject directly (allowing consumers to call unsubscribe() and break the stream) or wrap it in a service that exposes only an Observable via asObservable() to enforce least-privilege subscription. BehaviorSubject and ReplaySubject inherit the same unsubscription behavior while also losing their replay semantics after unsubscribe().
What encapsulation strategies effectively prevent external callers from tearing down a shared Subject without sacrificing multicast semantics? Does wrapping with asObservable() introduce any observable contract differences for downstream operators? Are there RxJS 7+ configuration options or patterns that mitigate this footgun without custom wrapper classes?
35775 reputation · 18 Sept 2026, 14:52 UTC
The error means someone called unsubscribe() on the Subject instance itself, not on a Subscription returned by subscribe(). That call is terminal for the whole multicast stream: it marks the Subject closed and drops its observer list, so every later next(), error(), or complete() throws ObjectUnsubscribedError. The fix is encapsulation, not configuration — keep the Subject private and hand out subject.asObservable(). RxJS 7+ Subject config flags do not guard against this.
Worth confirming against your installed version, because the premise that next() tests this.observers.length === 0 is a common but imprecise description. In RxJS 6 and 7 the throw is driven by the Subject's closed state, and unsubscribe() is what sets it:
// shape of the relevant behaviour, not a verbatim source excerpt
subject.unsubscribe(); // isStopped = true, closed = true, observers = null
subject.next(1); // throws ObjectUnsubscribedError("object unsubscribed")This also explains an asymmetry people trip over: complete() sets isStopped but leaves closed false, so a later next() is a silent no-op. unsubscribe() sets closed, so a later next() throws. Verify with a short script rather than trusting either description.
asObservable() — the default. The Subject never leaves the owning service or class.emit(value) instead of the Subject, if callers legitimately need to push values.share() or shareReplay() over a source — the internal Subject is not reachable from the returned Observable, so callers cannot unsubscribe it.complete() or unsubscribe() on the Subject, typically in a lifecycle hook.None of these change multicast semantics. All subscribers still share one execution.
For subscription and emission, no. Downstream operators see the same values, the same completion, and the same error propagation. The only difference is surface area: the returned value is a plain Observable, so next, error, complete, and unsubscribe are absent. That is the point.
| Call | On Subject | On asObservable() result |
|---|---|---|
subscribe() | works | works, same emissions |
pipe(...) | works | works, identical |
next() | works | not a function |
unsubscribe() | tears down the stream | not a function (TypeError) |
Two caveats. First, asObservable() protects only if the Subject reference itself stays private; returning it from a getter or stashing it on a public object reopens the hole. Second, it does not prevent leaks — a caller can still subscribe and never unsubscribe. That is a different problem, usually solved with takeUntil or a destroy-signal helper.
new Subject({ resetOnError, resetOnComplete, resetOnRefCountZero }) controls internal reset behaviour used by ref-counted sharing. It is not an access guard. A Subject constructed with any combination of those flags still throws ObjectUnsubscribedError after an external unsubscribe(). There is no built-in "unsubscribable: false" option.
const s = new Subject();
const o = s.asObservable();
console.log(typeof o.next); // "undefined"
console.log(typeof s.unsubscribe); // "function"
const sub = o.subscribe(v => console.log("got", v));
s.next(1); // got 1
s.unsubscribe();
try { s.next(2); } catch (e) { console.log(e.name, e.message); }
// ObjectUnsubscribedError object unsubscribed
sub.unsubscribe(); // safe: removes one observer onlyTo prove downstream parity, subscribe both directly and via asObservable(), pipe the same map, and compare emitted sequences before unsubscription. They should match.
Is the reference being unsubscribed the Subject itself, or a Subscription returned by subscribe()? If it is a Subscription, the bug is elsewhere — likely a shared reference or a teardown hook unsubscribing something it does not own — and encapsulation will not fix it. If it is the Subject, the private-field-plus-asObservable() pattern is the whole fix.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.