EMPTY vs NEVER#
Let's clarify the difference between the RxJS constants EMPTY and NEVER. Both are simple, pre-defined Observable constants, but they represent very different stream behaviors, primarily concerning completion.
Think of it like this: Both represent a stream that will never give you any data (no next emissions). The difference lies in whether they tell you they are finished or just stay silent forever.
-
EMPTY- What it does: Represents an Observable that emits zero items.
- Key Behavior: As soon as you subscribe to it, it immediately sends a
completenotification. - Analogy: It's like a function that returns immediately without doing anything (
return;), or reading an empty file. It quickly signals "I have nothing to give you, and I'm done." - Use Case: Useful when you need an Observable that does nothing but signal successful completion right away. This is often helpful in conditional logic within higher-order mapping operators (like
switchMap,mergeMap,concatMap). For example, if a condition isn't met, you might returnEMPTYinstead of making an API call, indicating that the operation for that specific trigger completed successfully without producing a value.
-
NEVER- What it does: Represents an Observable that emits zero items.
- Key Behavior: It never sends a
completenotification and never sends anerrornotification. It remains silent indefinitely after subscription. - Analogy: It's like a process that hangs forever without producing output or terminating, or a phone line that just keeps ringing and ringing without ever being answered or going to an error state. It signals "I have nothing for you right now, and I might never have anything, and I'm certainly not finished."
- Use Case: Represents a stream that simply never emits or terminates. This can be useful in testing scenarios or when you want to keep a combination operator (like
raceorcombineLatest) alive, even if one of its potential sources will never produce a relevant value or complete. It effectively keeps that "slot" open indefinitely without signalling completion or error. It can also be used intentionally to prevent parts of an Observable chain from completing.
At a glance
- Signature:
EMPTYandNEVERare constants, not functions; import and use them directly - Use
EMPTYwhen: a branch should produce nothing but still complete, typically insideswitchMap/concatMapconditionals orcatchError - Use
NEVERwhen: a stream must stay open silently: tests, keeping combinators alive, deliberately preventing completion - Top gotcha: returning
NEVERwhereEMPTYwas meant leaves queues (concatMap) and joins (forkJoin) waiting forever
Direct Comparison#
| Feature | EMPTY |
NEVER |
|---|---|---|
next emissions |
0 | 0 |
complete |
Yes (immediately) | No (never) |
error |
No (by default) | No (never) |
| Terminates? | Yes (completes immediately) | No (runs indefinitely) |
Minimal Example#
import { EMPTY, NEVER } from "rxjs";
EMPTY.subscribe({
next: () => console.log("next"), // never runs
complete: () => console.log("EMPTY: complete"), // runs immediately
});
NEVER.subscribe({
next: () => console.log("next"), // never runs
complete: () => console.log("complete"), // never runs either
});
// Output: "EMPTY: complete" and nothing else, ever
Code Snippet Demonstration#
import { Component, OnInit, OnDestroy } from "@angular/core";
import { EMPTY, NEVER, Subscription } from "rxjs";
@Component({
selector: "app-empty-never-demo",
template: `
<h4>EMPTY vs NEVER Demo</h4>
<p>Check the console log.</p>
<p>EMPTY Status: {{ emptyStatus }}</p>
<p>NEVER Status: {{ neverStatus }}</p>
`,
})
export class EmptyNeverDemoComponent implements OnInit, OnDestroy {
emptyStatus = "Subscribing...";
neverStatus = "Subscribing...";
private emptySub: Subscription | undefined;
private neverSub: Subscription | undefined;
ngOnInit(): void {
console.log(`--- Subscribing to EMPTY ---`);
this.emptySub = EMPTY.subscribe({
next: () => {
console.log("EMPTY: next (This will not be logged)");
this.emptyStatus = "Got next (unexpected)";
},
error: (err) => {
console.error("EMPTY: error", err);
this.emptyStatus = `Error: ${err}`;
},
complete: () => {
// This is called immediately!
console.log(
`EMPTY: complete! (Called immediately) at ${new Date().toLocaleTimeString()}`,
);
this.emptyStatus = "Completed immediately";
},
});
// The line below will likely log 'Completed immediately' because EMPTY completes synchronously
console.log(
`Current EMPTY status after sync subscribe: ${this.emptyStatus}`,
);
console.log(`\n--- Subscribing to NEVER ---`);
this.neverSub = NEVER.subscribe({
next: () => {
console.log("NEVER: next (This will not be logged)");
this.neverStatus = "Got next (unexpected)";
},
error: (err) => {
console.error("NEVER: error (This will not be logged)");
this.neverStatus = `Error: ${err}`;
},
complete: () => {
// This is never called!
console.log("NEVER: complete! (This will never be logged)");
this.neverStatus = "Completed (unexpected)";
},
});
// NEVER does nothing, so status remains 'Subscribing...'
console.log(
`Current NEVER status after sync subscribe: ${this.neverStatus}`,
);
// Set a timeout just to show NEVER doesn't complete on its own
setTimeout(() => {
if (this.neverStatus === "Subscribing...") {
console.log(
"\nAfter 2 seconds, NEVER still hasn't emitted or completed.",
);
this.neverStatus = "Still running after 2s (as expected)";
}
}, 2000);
}
ngOnDestroy(): void {
console.log("\n--- Component Destroying ---");
if (this.emptySub && !this.emptySub.closed) {
// This check is usually false as EMPTY completes immediately
console.log("Unsubscribing from EMPTY (already closed likely).");
this.emptySub.unsubscribe();
} else {
console.log("EMPTY subscription was already closed.");
}
if (this.neverSub && !this.neverSub.closed) {
// This is important for NEVER if the component is destroyed
console.log("Unsubscribing from NEVER.");
this.neverSub.unsubscribe();
}
}
}
Summary#
Choose EMPTY when you need an Observable that does nothing but signals successful completion instantly.
Choose NEVER when you need an Observable that does nothing and never signals completion or error.
Interview Q&A#
Where does EMPTY show up in real Angular code?
Mostly in two places: as a conditional no-op inside higher-order mapping (switchMap(x => valid(x) ? this.http.post(...) : EMPTY)) and as a "swallow the error, emit nothing" recovery in catchError(() => EMPTY). In both, downstream completion semantics stay healthy because EMPTY completes.
What breaks if you use NEVER instead of EMPTY inside concatMap?
concatMap waits for each inner Observable to complete before starting the next. NEVER never completes, so the queue stalls permanently: every later source value waits behind a stream that will not end. The same trap freezes forkJoin and delays combineLatest first emissions.
Does subscribing to EMPTY require cleanup?
No; it completes synchronously on subscribe, which closes the subscription. NEVER is the opposite: it never terminates itself, so the subscriber must unsubscribe (or use takeUntil-style operators) to release it.
Related#
- of, whose zero-argument form behaves like EMPTY
- concatMap and forkJoin, where the completion difference matters most
- catchError, a frequent EMPTY call site