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)
: ThrowsTimeoutError
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
?#
- 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,loading
is set totrue
. - The
SlowDataService.fetchData
method 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 thefetchData
Observable to emit anext
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. Thetimeout
operator's timer reaches 5000ms beforefetchData
emits anything.timeout
stops 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 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 forfinalize
.
- We use
finalize(() => { this.loading.set(false); })
: This runs reliably after the stream terminates (either successfully afternext
, or aftercatchError
handles theTimeoutError
or any other error), ensuring the loading indicator is hidden.takeUntilDestroyed
: Standard automatic unsubscription.- The
subscribe
block updates thefetchedData
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.