finalize and complete - Resource Release and Stream Completion Processing
In RxJS, it is important to properly manage stream completion and resource release. This page explains how the finalize operator and complete notifications work.
finalize - Operator for Resource Release
The finalize operator is the operator that executes the specified cleanup code when the Observable exits complete, error, or unsubscribe. finalize is always called only once at the end of the stream and never more than once.
🌐 RxJS Official Documentation - finalize
Basic Usage of finalize
import { of } from 'rxjs';
import { finalize, tap } from 'rxjs';
// Variable to manage loading state
let isLoading = true;
// Stream that succeeds
of('data')
.pipe(
tap((data) => console.log('Processing data:', data)),
// Executed in all cases: success, failure, or cancellation
finalize(() => {
isLoading = false;
console.log('Loading state reset:', isLoading);
})
)
.subscribe({
next: (value) => console.log('Value:', value),
complete: () => console.log('Complete'),
});
// Output:
// Processing data: data
// Value: data
// Complete
// Loading state reset: falsefinalize on Error
import { throwError } from 'rxjs';
import { finalize, catchError } from 'rxjs';
let isLoading = true;
throwError(() => new Error('Data fetch error'))
.pipe(
catchError((err) => {
console.error('Error handling:', err.message);
throw err; // Re-throw error
}),
finalize(() => {
isLoading = false;
console.log('Resource release after error:', isLoading);
})
)
.subscribe({
next: (value) => console.log('Value:', value),
error: (err) => console.error('Error in subscriber:', err.message),
complete: () => console.log('Complete'), // Not called on error
});
// Output:
// Error handling: Data fetch error
// Error in subscriber: Data fetch error
// Resource release after error: falsefinalize on Unsubscribe
import { interval } from 'rxjs';
import { finalize } from 'rxjs';
let resource = 'Active';
// Count every second
const subscription = interval(1000)
.pipe(
finalize(() => {
resource = 'Released';
console.log('Resource state:', resource);
})
)
.subscribe((count) => {
console.log('Count:', count);
// Manually unsubscribe after counting 3 times
if (count >= 2) {
subscription.unsubscribe();
}
});
// Output:
// Count: 0
// Count: 1
// Count: 2
// Resource state: ReleasedFinalize is useful not only when an error occurs, but also when you want to ensure cleanup processing upon successful completion or manual unsubscribe.
complete - Notification of Successful Completion of Stream
When an Observable finishes successfully, the Observer's complete callback is invoked. This is the last step in the Observable's life cycle.
Automatic complete
Some Observables complete automatically when certain conditions are met.
import { of } from 'rxjs';
import { take } from 'rxjs';
// Finite sequences complete automatically
of(1, 2, 3).subscribe({
next: (value) => console.log('Value:', value),
complete: () => console.log('Finite stream complete'),
});
// Stream limited with interval + take
interval(1000)
.pipe(
take(3) // Complete after obtaining 3 values
)
.subscribe({
next: (value) => console.log('Count:', value),
complete: () => console.log('Limited stream complete'),
});
// Output:
// Value: 1
// Value: 2
// Value: 3
// Finite stream complete
// Count: 0
// Count: 1
// Count: 2
// Limited stream completeManual complete
For Subject and custom ones, complete can be called manually.
import { Subject } from 'rxjs';
const subject = new Subject<number>();
subject.subscribe({
next: (value) => console.log('Value:', value),
complete: () => console.log('Subject complete'),
});
subject.next(1);
subject.next(2);
subject.complete(); // Complete manually
subject.next(3); // Ignored after completion
// Output:
// Value: 1
// Value: 2
// Subject completeDifference between finalize and complete
It is important to understand the important differences.
Execution Timing
complete: called only when an Observable completes successfullyfinalize: called when the Observable terminates with completion, error, or unsubscribe
Usage
complete: Receive notification of a successful completion (processing on success)finalize: Ensure that resources are released or cleaned up (processing that must be performed regardless of success or failure)
Practical Use Cases
API Calls and Loading State Management
import { ajax } from 'rxjs/ajax';
import { finalize, catchError } from 'rxjs';
import { of } from 'rxjs';
// Loading state
let isLoading = false;
function fetchData(id: string) {
// Start loading
isLoading = true;
const loading = document.createElement('p');
loading.style.display = 'block';
document.body.appendChild(loading);
// document.getElementById('loading')!.style.display = 'block';
// API request
return ajax.getJSON(`https://jsonplaceholder.typicode.com/posts/${id}`).pipe(
catchError((error) => {
console.error('API error:', error);
return of({ error: true, message: 'Failed to retrieve data' });
}),
// End loading regardless of success or failure
finalize(() => {
isLoading = false;
loading!.style.display = 'none';
console.log('Loading state reset complete');
})
);
}
// Usage example
fetchData('123').subscribe({
next: (data) => console.log('Data:', data),
complete: () => console.log('Data fetch complete'),
});
// Output:
// API error: AjaxErrorImpl {message: 'ajax error', name: 'AjaxError', xhr: XMLHttpRequest, request: {…}, status: 0, …}
// Data: {error: true, message: 'Failed to retrieve data'}
// Data fetch complete
// Loading state reset complete
// GET https://jsonplaceholder.typicode.com/posts/123 net::ERR_NAME_NOT_RESOLVEDResource Cleanup
import { interval } from 'rxjs';
import { finalize, takeUntil } from 'rxjs';
import { Subject } from 'rxjs';
class ResourceManager {
private destroy$ = new Subject<void>();
private timerId: number | null = null;
constructor() {
// Initialize some resource
this.timerId = window.setTimeout(() => console.log('Timer execution'), 10000);
// Periodic processing
interval(1000)
.pipe(
// Stop on component destruction
takeUntil(this.destroy$),
// Ensure resource release
finalize(() => {
console.log('Interval stopped');
})
)
.subscribe((count) => {
console.log('Running...', count);
});
}
dispose() {
// Dispose processing
if (this.timerId) {
window.clearTimeout(this.timerId);
this.timerId = null;
}
// Stream stop signal
this.destroy$.next();
this.destroy$.complete();
console.log('Resource manager disposal complete');
}
}
// Usage example
const manager = new ResourceManager();
// Dispose after 5 seconds
setTimeout(() => {
manager.dispose();
}, 5000);
// Output:
// Running... 0
// Running... 1
// Running... 2
// Running... 3
// Running... 4
// Interval stopped
// Resource manager disposal completeBest Practices
- Always free resources: use
finalizeto ensure cleanup when the stream ends - Loading state management: always reset loading state using
finalize - Component lifecycle management: use
takeUntilin combination withfinalizeto clean up resources when components are destroyed (this pattern is recommended, especially for Angular) - Use with error handling: Combine
catchErrorandfinalizeto provide fallback handling and reliable cleanup after errors - Knowing completion status: use
completecallback to determine if the stream has completed successfully
Summary
finalize and complete are important tools for resource management and processing completion in RxJS. finalize is ideal for resource release because it ensures that the stream is executed no matter how it terminates. On the other hand, complete is used when you want to perform normal exit processing. By combining these tools appropriately, you can prevent memory leaks and build reliable applications.