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 (
timeoutoperator). - 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 (
TimeoutErroris 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): ThrowsTimeoutErrorif 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?#
- Preventing Indefinite Waits: Protects your application from hanging if a backend service or other asynchronous source becomes unresponsive.
- Improving User Experience: Provides timely feedback (an error message) to the user instead of leaving them staring at a loading spinner forever.
- 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:
- When
getData()is called,loadingis set totrue. - The
SlowDataService.fetchDatamethod is called, which returns an Observable that will emit data after the specifiedresponseTimeMs. timeout(this.apiTimeoutMs): This operator starts its internal timer (5000ms). It waits for thefetchDataObservable to emit anextnotification.- Scenario 1 (Fetch Fast Data - 500ms): The
fetchDataObservable emits data after 500ms. This is well within the 5000ms timeout.timeoutsees the emission, cancels its internal timer, and passes the data along the stream. The data is displayed. - Scenario 2 (Fetch Slow Data - 6000ms): The
fetchDataObservable is set to respond after 6000ms. Thetimeoutoperator's timer reaches 5000ms beforefetchDataemits anything.timeoutstops waiting, throws aTimeoutError, and terminates the source subscription.
- Scenario 1 (Fetch Fast Data - 500ms): The
catchError((err) => ...): This catches any error, including theTimeoutError.- We use
instanceof TimeoutErrorto specifically check if the error was due to the timeout. - We set an appropriate
errorMessagebased on the error type. - We return
EMPTYto ensure the stream completes gracefully forfinalize.
- We use
finalize(() => { this.loading.set(false); }): This runs reliably after the stream terminates (either successfully afternext, or aftercatchErrorhandles theTimeoutErroror any other error), ensuring the loading indicator is hidden.takeUntilDestroyed: Standard automatic unsubscription.- The
subscribeblock updates thefetchedDatasignal 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.