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#
- Delays Emissions: It delays when the values/signals are sent to the next operator or subscriber.
- Doesn't Delay Subscription: The subscription to the source happens immediately; only the emissions are postponed.
- Applies to All Notifications: It delays
next
,error
, andcomplete
. - Input: Takes a duration in milliseconds (e.g.,
delay(500)
) or a specific futureDate
.
Why Use delay
?#
- 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. - Testing/Debugging: Introduce artificial latency into streams to test how your application handles timing issues or loading states.
- 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:
- When
saveData()
is called,saving
is set totrue
, showing the "Saving..." message immediately. mockSaveOperation()
is called. It simulates a quick backend response (completes in ~100ms) usingof(...)
anddelay(100)
.- The result (or error) from
mockSaveOperation
flows into the component's RxJS pipe. - The first
tap
logs the immediate result from the "backend". 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.- After the 750ms delay:
- If successful: The
next
notification proceeds to thesubscribe
block'snext
handler. The success message is displayed. - If an error occurred: The
error
notification proceeds to thecatchError
operator. The error message is displayed.
- If successful: The
finalize
: This runs after the delayednext
orerror
has been processed (or if the stream completes/unsubscribes). It setssaving
tofalse
, hiding the "Saving..." message.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.