Skip to content

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.

  1. EMPTY

    • What it does: Represents an Observable that emits zero items.
    • Key Behavior: As soon as you subscribe to it, it immediately sends a complete notification.
    • 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 return EMPTY instead of making an API call, indicating that the operation for that specific trigger completed successfully without producing a value.
  2. NEVER

    • What it does: Represents an Observable that emits zero items.
    • Key Behavior: It never sends a complete notification and never sends an error notification. 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 race or combineLatest) 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.

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)

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 at ${new Date().toLocaleTimeString("en-IN", {
        timeZone: "Asia/Kolkata",
      })} (IST).
    </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.