Skip to content

delay

The delay operator simply shifts the emission of each notification (next, error, complete) from its source Observable forward in time by a specified duration.

Think of it like scheduled mail delivery:

  • The source Observable "drops a letter in the mailbox" (next emission occurs).
  • The delay operator picks it up but holds onto it.
  • It waits for the specified time (e.g., 500 milliseconds).
  • Then, it delivers the letter (emits the next value) downstream.

The same happens for error and complete signals – they are also held for the specified duration before being passed on.

Key Points#

  1. Delays Emissions: It delays when the values/signals are sent to the next operator or subscriber.
  2. Doesn't Delay Subscription: The subscription to the source happens immediately; only the emissions are postponed.
  3. Applies to All Notifications: It delays next, error, and complete.
  4. Input: Takes a duration in milliseconds (e.g., delay(500)) or a specific future Date.

Why Use delay?#

  1. UI Polish: Simulate a minimum processing time. For example, if saving data is extremely fast, a "Saving..." message might just flash on and off. Using delay can ensure the message stays visible for at least, say, half a second, providing better user feedback.
  2. Testing/Debugging: Introduce artificial latency into streams to test how your application handles timing issues or loading states.
  3. Simple Sequencing (Less Common): Ensure a small pause before an action occurs after an event (though more complex sequencing often uses other operators).

Real-World Example: Minimum Display Time for a "Saved" Message#

Imagine clicking a "Save" button. The backend operation might be incredibly fast (e.g., 50ms). If you immediately show and then hide a "Saved!" confirmation, the user might not even register it. Let's ensure the "Saved!" message stays visible for at least 750ms.

Code Snippet#

import {
  Component,
  inject,
  signal,
  ChangeDetectionStrategy,
  DestroyRef,
} from "@angular/core";
import { CommonModule } from "@angular/common";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { Observable, of, timer } from "rxjs"; // Import 'of' and 'timer'
import { delay, switchMap, tap, finalize, catchError } from "rxjs/operators";
import { EMPTY } from "rxjs"; // Import EMPTY

// Mock Service Function (simulates a quick backend save)
function mockSaveOperation(): Observable<{
  success: boolean;
  timestamp: number;
}> {
  console.log("Backend: Starting simulated save...");
  const saveSuccess = Math.random() > 0.2; // Simulate occasional failure
  return of(saveSuccess).pipe(
    delay(100), // Simulate VERY FAST network/backend time (100ms)
    tap((success) =>
      console.log(
        `Backend: Simulated save ${success ? "successful" : "failed"}.`
      )
    ),
    switchMap((success) => {
      if (success) {
        return of({ success: true, timestamp: Date.now() });
      } else {
        // Simulate an error being returned from backend
        return timer(50).pipe(
          switchMap(() => {
            throw new Error("Save failed due to backend validation.");
          })
        );
      }
    })
  );
}

@Component({
  selector: "app-save-status",
  standalone: true,
  imports: [CommonModule],
  template: `
    <div>
      <h4>Save Example with Delay</h4>
      <button (click)="saveData()" [disabled]="saving()">Save Data</button>

      @if (saving()) {
      <p class="status saving">Saving...</p>
      } @else if (statusMessage()) {
      <p
        class="status"
        [class.success]="isSuccess()"
        [class.error]="!isSuccess()"
      >
        {{ statusMessage() }}
      </p>
      }
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SaveStatusComponent {
  private destroyRef = inject(DestroyRef);

  // --- State Signals ---
  saving = signal<boolean>(false);
  statusMessage = signal<string | null>(null);
  isSuccess = signal<boolean>(false);

  saveData(): void {
    if (this.saving()) return; // Prevent multiple saves

    this.saving.set(true);
    this.statusMessage.set(null); // Clear previous status
    console.log('UI: Save initiated, showing "Saving..."');

    const minimumDisplayTime = 750; // Ensure feedback shows for at least 750ms

    mockSaveOperation()
      .pipe(
        tap({
          next: (result) =>
            console.log("UI Stream: Save operation successful (before delay)"),
          error: (err) =>
            console.error("UI Stream: Save operation failed (before delay)"),
        }),

        // --- Apply the delay ---
        // Delay the NEXT or ERROR notification by minimumDisplayTime
        delay(minimumDisplayTime),
        // ---------------------

        catchError((err: Error) => {
          // Handle the error AFTER the delay
          console.error("UI: Handling error after delay:", err.message);
          this.isSuccess.set(false);
          this.statusMessage.set(`Error: ${err.message}`);
          // Return EMPTY to gracefully complete the stream for finalize
          return EMPTY;
        }),
        // finalize runs after delay + next/error/complete
        finalize(() => {
          console.log('UI: Finalizing save operation (hiding "Saving...")');
          this.saving.set(false);
        }),
        // Automatically unsubscribe when the component is destroyed
        takeUntilDestroyed(this.destroyRef)
      )
      .subscribe({
        next: (result) => {
          // Handle success AFTER the delay
          console.log(`UI: Displaying success message after delay.`);
          this.isSuccess.set(true);
          this.statusMessage.set(
            `Saved successfully at ${new Date(
              result.timestamp
            ).toLocaleTimeString()}`
          );
        },
        // Error is handled in catchError
        // Complete isn't strictly needed here as finalize covers the loading state change
      });
  }
}

Explanation:

  1. When saveData() is called, saving is set to true, showing the "Saving..." message immediately.
  2. mockSaveOperation() is called. It simulates a quick backend response (completes in ~100ms) using of(...) and delay(100).
  3. The result (or error) from mockSaveOperation flows into the component's RxJS pipe.
  4. The first tap logs the immediate result from the "backend".
  5. delay(minimumDisplayTime): This is the key part. If the backend responded successfully (next), delay holds that success notification for 750ms before passing it on. If the backend responded with an error, delay holds that error notification for 750ms.
  6. After the 750ms delay:
    • If successful: The next notification proceeds to the subscribe block's next handler. The success message is displayed.
    • If an error occurred: The error notification proceeds to the catchError operator. The error message is displayed.
  7. finalize: This runs after the delayed next or error has been processed (or if the stream completes/unsubscribes). It sets saving to false, hiding the "Saving..." message.
  8. takeUntilDestroyed: Standard cleanup.

Because of delay(750), even though the backend might respond in 100ms, the UI won't update with the final "Saved!" or "Error..." message, and the "Saving..." indicator won't disappear, until at least 750ms have passed since the backend responded. This gives the user time to perceive the feedback.