Skip to content

timeout

The timeout operator sets a time limit. If the source Observable doesn't emit its first value or complete within that specified duration, the timeout operator will cause the stream to emit a TimeoutError and terminate.

Think of it like setting an egg timer for an operation:

  • You start an operation (subscribe to the source Observable).
  • You start the timer (timeout operator).
  • If the operation finishes (emits a value or completes) before the timer goes off, everything is fine.
  • If the timer goes off before the operation finishes, the timer rings loudly (TimeoutError is emitted), and you stop waiting for the original operation.

Key Configurations & Behaviors#

You can configure timeout with a duration (milliseconds) or a specific Date. More advanced configurations allow specifying different timeouts for the first emission versus subsequent emissions, but the most common use is a single duration for the overall operation.

  • timeout(5000): Throws TimeoutError if the first emission doesn't arrive within 5 seconds of subscription.
  • timeout({ first: 5000, each: 1000 }): Throws if the first emission takes longer than 5s, OR if the time between any two subsequent emissions exceeds 1s. (Less common).
  • timeout({ each: 10000 }): Allows the first emission to take any amount of time, but throws if subsequent emissions are more than 10s apart.

Why Use timeout?#

  1. Preventing Indefinite Waits: Protects your application from hanging if a backend service or other asynchronous source becomes unresponsive.
  2. Improving User Experience: Provides timely feedback (an error message) to the user instead of leaving them staring at a loading spinner forever.
  3. Resource Management: Can help release resources tied up in waiting for a response that may never come.

Real-World Example: Setting a Timeout for an API Request#

A common scenario is fetching data from an external API. Sometimes, the network might be slow, or the API server itself might be experiencing issues. We want to limit how long we wait for a response before giving up.

Code Snippet#

1. Mock Data Service (Simulates Slow/Fast Responses)

import { Injectable } from "@angular/core";
import { Observable, of, timer } from "rxjs";
import { delay, switchMap, tap } from "rxjs/operators";

export interface ExternalData {
  id: string;
  value: number;
}

@Injectable({
  providedIn: "root",
})
export class SlowDataService {
  fetchData(id: string, responseTimeMs: number): Observable<ExternalData> {
    console.log(
      `Backend: Request received for ID ${id}. Will respond in ${responseTimeMs}ms.`
    );
    const data: ExternalData = { id: id, value: Math.random() * 100 };

    // Simulate the delay
    return of(data).pipe(
      delay(responseTimeMs),
      tap(() => console.log(`Backend: Responding for ID ${id}.`))
    );
  }
}

2. Data Fetching Component (Applies timeout)

import {
  Component,
  inject,
  signal,
  ChangeDetectionStrategy,
  DestroyRef,
} from "@angular/core";
import { CommonModule } from "@angular/common";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { SlowDataService, ExternalData } from "./slow-data.service"; // Adjust path
import { tap, timeout, catchError, finalize } from "rxjs/operators";
import { EMPTY, TimeoutError } from "rxjs"; // Import TimeoutError and EMPTY

@Component({
  selector: "app-data-fetcher",
  standalone: true,
  imports: [CommonModule],
  template: `
    <div>
      <h4>Data Fetcher with Timeout</h4>
      <button (click)="getData(500)" [disabled]="loading()">
        Fetch Fast Data (500ms)
      </button>
      <button (click)="getData(6000)" [disabled]="loading()">
        Fetch Slow Data (6000ms)
      </button>

      @if (loading()) {
      <p class="status loading">Loading data (Timeout set to 5s)...</p>
      } @if (errorMessage()) {
      <p class="status error">Error: {{ errorMessage() }}</p>
      } @if (fetchedData()) {
      <div class="data">
        <p>Data Received:</p>
        <pre>{{ fetchedData() | json }}</pre>
      </div>
      }
    </div>
  `,
  // No 'styles' section as per previous request
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DataFetcherComponent {
  private dataService = inject(SlowDataService);
  private destroyRef = inject(DestroyRef);

  // --- State Signals ---
  loading = signal<boolean>(false);
  fetchedData = signal<ExternalData | null>(null);
  errorMessage = signal<string | null>(null);

  private readonly apiTimeoutMs = 5000; // Set timeout to 5 seconds

  getData(responseTimeMs: number): void {
    if (this.loading()) return;

    this.loading.set(true);
    this.fetchedData.set(null);
    this.errorMessage.set(null);
    console.log(
      `UI: Initiating fetch. Response expected in ${responseTimeMs}ms. Timeout is ${this.apiTimeoutMs}ms.`
    );

    this.dataService
      .fetchData("item123", responseTimeMs)
      .pipe(
        tap((data) =>
          console.log(
            "UI Stream: Data received (before timeout check completed)"
          )
        ),

        // --- Apply the timeout ---
        timeout(this.apiTimeoutMs), // If no 'next' within 5000ms, throw TimeoutError
        // -----------------------

        catchError((err) => {
          console.error("UI Stream: Error caught.");
          // --- Check specifically for TimeoutError ---
          if (err instanceof TimeoutError) {
            console.error("Error Type: TimeoutError");
            this.errorMessage.set(
              `Operation timed out after ${this.apiTimeoutMs / 1000} seconds.`
            );
          } else {
            console.error("Error Type: Other", err);
            this.errorMessage.set(
              `An unexpected error occurred: ${err.message || err}`
            );
          }
          // Return EMPTY to allow finalize to run
          return EMPTY;
        }),
        finalize(() => {
          this.loading.set(false);
          console.log(
            "UI: Finalize block executed - Loading state set to false."
          );
        }),
        takeUntilDestroyed(this.destroyRef)
      )
      .subscribe({
        next: (data) => {
          console.log("UI: Subscribe next - updating data signal.");
          this.fetchedData.set(data);
        },
        // Error handled by catchError
        error: (err) => {
          /* Already handled */
        },
        complete: () => {
          console.log("UI: Subscribe complete.");
        },
      });
  }
}

Explanation:

  1. When getData() is called, loading is set to true.
  2. The SlowDataService.fetchData method is called, which returns an Observable that will emit data after the specified responseTimeMs.
  3. timeout(this.apiTimeoutMs): This operator starts its internal timer (5000ms). It waits for the fetchData Observable to emit a next notification.
    • Scenario 1 (Fetch Fast Data - 500ms): The fetchData Observable emits data after 500ms. This is well within the 5000ms timeout. timeout sees the emission, cancels its internal timer, and passes the data along the stream. The data is displayed.
    • Scenario 2 (Fetch Slow Data - 6000ms): The fetchData Observable is set to respond after 6000ms. The timeout operator's timer reaches 5000ms before fetchData emits anything. timeout stops waiting, throws a TimeoutError, and terminates the source subscription.
  4. catchError((err) => ...): This catches any error, including the TimeoutError.
    • We use instanceof TimeoutError to specifically check if the error was due to the timeout.
    • We set an appropriate errorMessage based on the error type.
    • We return EMPTY to ensure the stream completes gracefully for finalize.
  5. finalize(() => { this.loading.set(false); }): This runs reliably after the stream terminates (either successfully after next, or after catchError handles the TimeoutError or any other error), ensuring the loading indicator is hidden.
  6. takeUntilDestroyed: Standard automatic unsubscription.
  7. The subscribe block updates the fetchedData signal only if the operation completed successfully within the timeout period.

By using timeout, you make your data fetching more robust against unresponsive services, leading to a better user experience.