finalize
The finalize
operator lets you specify a callback function that will be executed when the source Observable terminates. Termination happens in one of three ways:
- The Observable completes successfully (sends its last value and the
complete
notification). - The Observable emits an error notification.
- The subscription to the Observable is unsubscribed (e.g., manually, or automatically via operators like
take
,takeUntil
, ortakeUntilDestroyed
).
Think of it like the finally
block in a traditional try...catch...finally
statement. The code inside finalize
is guaranteed to run after the Observable finishes its work or is stopped, regardless of why it stopped (success, error, or unsubscription).
Key Points#
- Guaranteed Execution on Termination: Runs whether the stream succeeds, fails, or is unsubscribed.
- No Arguments: The callback function you provide to
finalize
receives no arguments. It doesn't know if an error occurred or what the last value was; it just knows the stream is done. - Side Effects Only: Like
tap
,finalize
is purely for side effects. It doesn't affect the values, errors, or completion signals passing through the stream (because it runs after them). - Ideal for Cleanup: Its primary purpose is resource cleanup or actions that must happen when an operation is finished.
Why Use finalize
?#
The most common and important use case is managing loading states.
- You start an operation (e.g., HTTP request).
- You set a
loading
flag/signal totrue
. - The operation might succeed or fail.
- You need to ensure the
loading
flag/signal is always set back tofalse
when the operation is finished, no matter the outcome.finalize
is perfect for this.
Other uses include:
- Closing connections (though often handled by unsubscription itself).
- Logging the end of an operation.
- Releasing any temporary resources acquired at the start of the subscription.
Real-World Example: Managing Loading State for Data Fetching#
This is the classic example. We fetch data, show a loading indicator, and use finalize
to hide the indicator when the fetch completes or fails.
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 { delay, switchMap, tap, catchError, finalize } from "rxjs/operators";
import { EMPTY } from "rxjs"; // Import EMPTY
// --- Mock Data Service ---
interface Product {
id: number;
name: string;
price: number;
}
function mockFetchProducts(
failRequest: boolean = false
): Observable<Product[]> {
console.log("Backend: Starting simulated product fetch...");
if (failRequest) {
// Simulate a delayed error
return timer(1200).pipe(
tap(() => console.log("Backend: Simulating network error...")),
switchMap(() => {
throw new Error("Network Error: Failed to connect to server.");
})
);
} else {
// Simulate a successful response with delay
const products: Product[] = [
{ id: 101, name: "Super Widget", price: 19.99 },
{ id: 102, name: "Mega Gadget", price: 29.95 },
];
return of(products).pipe(
delay(1500), // Simulate network latency
tap(() => console.log("Backend: Simulated fetch successful."))
);
}
}
// --- End Mock Data Service ---
@Component({
selector: "app-product-list",
standalone: true,
imports: [CommonModule],
template: `
<div>
<h4>Product List (Finalize Example)</h4>
<button (click)="loadProducts(false)" [disabled]="loading()">
Load Products
</button>
<button (click)="loadProducts(true)" [disabled]="loading()">
Load Products (Simulate Error)
</button>
@if (loading()) {
<p class="status loading">Loading products...</p>
} @if (errorMessage()) {
<p class="status error">Error: {{ errorMessage() }}</p>
} @if (products().length > 0 && !loading()) {
<ul>
@for(product of products(); track product.id) {
<li>{{ product.name }} - {{ product.price | currency }}</li>
}
</ul>
} @else if (!loading() && !errorMessage()) {
<p>Click button to load.</p>
}
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductListComponent {
private destroyRef = inject(DestroyRef);
// --- State Signals ---
loading = signal<boolean>(false);
products = signal<Product[]>([]);
errorMessage = signal<string | null>(null);
loadProducts(simulateError: boolean = false): void {
if (this.loading()) return; // Don't load if already loading
this.loading.set(true); // <-- Start Loading Indicator
this.products.set([]);
this.errorMessage.set(null);
console.log("UI: Fetch initiated, showing loading state.");
mockFetchProducts(simulateError)
.pipe(
tap((data) =>
console.log("UI Stream: Received product data (before finalize)")
),
catchError((err: Error) => {
console.error("UI Stream: Error caught:", err.message);
this.errorMessage.set(err.message || "Could not load products.");
// Return EMPTY to allow finalize to run after error handling
return EMPTY;
}),
// --- Key Operator ---
// This block runs AFTER success (tap), OR AFTER error (catchError),
// OR if the subscription is cancelled (e.g., by takeUntilDestroyed).
finalize(() => {
this.loading.set(false); // <-- Stop Loading Indicator
console.log(
"UI: Finalize block executed - Loading state set to false."
);
}),
// --------------------
takeUntilDestroyed(this.destroyRef) // Ensure unsubscription on destroy
)
.subscribe({
next: (data) => {
console.log("UI: Subscribe next - updating product list.");
this.products.set(data);
},
// Error already handled by catchError
error: (err) => {
/* No need for code here usually if catchError handles UI state */
},
// Complete isn't needed for loading state because finalize covers it
complete: () => {
console.log("UI: Subscribe complete.");
},
});
}
}
Explanation:
- When
loadProducts
is called,loading
is immediately set totrue
, displaying the "Loading products..." message. mockFetchProducts
returns an Observable that simulates either success or failure after a delay.- The
pipe
chain processes the result:tap
: Logs successful data receipt (only runs on success).catchError
: Catches any error from the source. It sets theerrorMessage
signal and returnsEMPTY
. ReturningEMPTY
makes the stream complete gracefully after the error, ensuringfinalize
still runs.finalize(() => { this.loading.set(false); })
: This is the crucial part. This callback function is registered to run when the stream terminates.- If
mockFetchProducts
succeeds, thenext
value passes throughtap
, then the stream completes.finalize
runs, settingloading
tofalse
. - If
mockFetchProducts
fails, the error goes tocatchError
.catchError
handles it and returnsEMPTY
, which immediately completes the stream.finalize
runs, settingloading
tofalse
. - If the component is destroyed while the fetch is in progress,
takeUntilDestroyed
triggers unsubscription.finalize
runs, settingloading
tofalse
. takeUntilDestroyed
: Handles automatic unsubscription.
- The
subscribe
block'snext
handler updates theproducts
signal only on success.
No matter what happens – success, failure, or component destruction – the finalize
block ensures that this.loading.set(false)
is called, correctly cleaning up the UI loading state. This makes it much more reliable than trying to manage the loading flag in both the error
and complete
/next
handlers of the subscribe
block.