Your Angular app just crashed in production with ‘ERROR Error: Uncaught (in promise): [object Object]’. Sound familiar? After debugging countless production fires, I’ve learned that proper error handling isn’t optional—it’s the difference between sleeping through the night and getting paged at 3 AM.
Introduction
I spent three hours last week debugging a production error that turned out to be a simple null reference. Three. Hours. The worst part? If we’d had proper error handling in place, it would’ve taken three minutes.
Angular’s error handling is fundamentally different from vanilla JavaScript. Zone.js intercepts everything, the dependency injection system has its own error propagation rules, and async operations require special handling. Get it wrong, and you’re flying blind in production.
This guide covers everything you need to implement production-ready error handling in Angular. You’ll learn to catch errors before users see them, debug issues with actual context, and sleep better knowing your app won’t silently fail.
Angular Error Handling Fundamentals
Understanding How Angular Handles Errors
Angular’s error handling works differently than vanilla JavaScript. In traditional Angular apps with Zone.js, the framework intercepts all async operations and can catch errors that would normally disappear. In modern zoneless Angular, error handling is more explicit but also more predictable.
Here’s what happens when an error occurs:
With Zone.js (Old School Angular):
- Zone.js catches the error in its error handler
- Angular’s error handler receives it from Zone
Without Zone.js (Modern Shiny Angular):
- Synchronous errors bubble up normally OR Async errors must be explicitly caught
- ErrorHandler still catches uncaught errors
The key insight: Angular catches errors but doesn’t handle them. That’s your job, regardless of your change detection strategy.
A Production-Ready Angular Global Error Handler
Let’s build a basic GlobalErrorHandler that actually helps during debugging:
import { ErrorHandler, Injectable } from '@angular/core';
import { TrackJS } from 'trackjs';
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
handleError(error: Error): void {
// Don't just log it - that's what Angular already does
console.warn('🔥 Error:', error.message, error.stack);
// Send to TrackJS for production monitoring
TrackJS.track(error);
}
}
Register it in your app config (standalone) or module:
// For standalone apps (Angular 14+)
bootstrapApplication(AppComponent, {
providers: [
{ provide: ErrorHandler, useClass: GlobalErrorHandler }
]
});
// For module-based apps
@NgModule({
providers: [
{ provide: ErrorHandler, useClass: GlobalErrorHandler }
]
})
export class AppModule { }
Want to see this in action? Check out our Angular TODOMVC example with TrackJS integration or dive into the complete Angular integration docs for more advanced patterns.
Angular HTTPClient Error Handling
HTTP errors need special handling because there not really JavaScript errors–they are network errors. To deal with them, you need to intercept the failures globally. If you use TrackJS JavaScript Error Monitoring, this happens automatically, but if not, here’s a simple way to capture these errors and record that they happened:
import { HttpInterceptor, Injectable } from '@angular/core';
import { catchError, throwError } from 'rxjs/operators';
@Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
catchError((error: HTTPErrorResponse) => {
console.warn('🔥 Error:', error.error.message);
return throwError(() => transformedError);
});
);
}
}
// For standalone apps (Angular 14+)
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
withInterceptors([HttpErrorInterceptor]),
)
]
});
Types of Angular Errors (And How to Handle Each)
Most Angular applications tend to fail in the same ways. Here are the most common kinds of failures I’ve seen with Angular
1. Runtime TypeError
The most common Angular error. Usually means you’re accessing properties on null/undefined:
// BAD - This will throw TypeError in production
export class UserComponent {
user: User;
ngOnInit() {
// If API fails, user stays undefined
this.userService.getUser().subscribe(user => this.user = user);
}
getUserName(): string {
return this.user.name; // 💥 TypeError: Cannot read property 'name' of undefined
}
}
// GOOD - Defensive programming
export class UserComponent {
user?: User;
getUserName(): string {
return this.user?.name ?? 'Guest'; // Safe with optional chaining
}
}
2. Circular Dependency Errors
Angular’s DI system hates circular dependencies. I’ve been dealing with this since “DLL HELL” back in my old Windows days. They’re sneaky and crash at runtime:
// This creates a circular dependency
@Injectable()
export class ServiceA {
constructor(private serviceB: ServiceB) {} // A needs B
}
@Injectable()
export class ServiceB {
constructor(private serviceA: ServiceA) {} // B needs A 💥
}
// Lazy FIX 1: Use Injector for lazy resolution.
@Injectable()
export class ServiceA {
private serviceB: ServiceB;
constructor(private injector: Injector) {
// Lazy load to break the cycle
setTimeout(() => {
this.serviceB = this.injector.get(ServiceB);
});
}
}
// FIX 2: Refactor to remove the cycle (better)
@Injectable()
export class SharedService {
// Move shared logic here
}
3. Missing Provider Errors
“No provider for X!” - Every Angular developer’s nightmare:
// Common mistake: Forgetting to provide a service
export class FeatureComponent {
constructor(private featureService: FeatureService) {} // 💥 NullInjectorError
}
// FIX: Provide at appropriate level
@Component({
selector: 'app-feature',
providers: [FeatureService], // Component level
// OR
})
export class FeatureComponent {}
// OR provide in module/standalone config
@Injectable({ providedIn: 'root' }) // Application level
export class FeatureService {}
4. Template Parsing Errors
Template errors are caught at compile time with strict templates (use them!):
// angular.json
{
"angularCompilerOptions": {
"strictTemplates": true,
"strictNullChecks": true
}
}
// Now this catches errors at build time:
<!-- BAD - Caught by compiler -->
<div>{{ user.naem }}</div> <!-- Property 'naem' does not exist -->
<!-- GOOD - Safe with null checks -->
<div>{{ user?.name || 'Loading...' }}</div>
5. HTTP Errors (Angular HTTP Error Handling)
HTTP errors need special attention because they’re async and carry status codes. Usually there are some 4xx errors that are expected, but when things go down, you might get a 500, or weird response data.
@Injectable()
export class ApiService {
getUser(id: string): Observable<User> {
return this.http.get<User>(`/api/users/${id}`).pipe(
catchError(error => {
// Handle different HTTP status codes
if (error.status === 404) {
// User not found - return default
return of(GUEST_USER);
}
if (error.status === 401) {
// Unauthorized - redirect to login
this.router.navigate(['/login']);
return EMPTY;
}
// Let global handler deal with other errors
return throwError(() => error);
})
);
}
}
6. Memory Leaks (Unsubscribed Observables)
Not technically an error, but causes them eventually:
// BAD - Memory leak
export class LeakyComponent {
ngOnInit() {
interval(1000).subscribe(() => {
console.log('Still running after component destroyed!');
});
}
}
// GOOD - Clean up subscriptions
export class CleanComponent implements OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
interval(1000).pipe(
takeUntil(this.destroy$)
).subscribe(() => {
console.log('Stops when component destroyed');
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
7. Infinite Loop Errors
Change detection loops will freeze your app (especially dangerous with Zone.js):
// BAD - Creates infinite loop
export class LoopyComponent {
get randomNumber() {
return Math.random(); // Changes every detection cycle!
}
}
// Template
<div></div> <!-- Infinite change detection -->
// GOOD - Stable values
export class StableComponent {
randomNumber = Math.random(); // Set once
regenerate() {
this.randomNumber = Math.random(); // Update explicitly
}
}
// Note: With OnPush or Signals, this is less likely but still possible
// Signals actually help prevent this:
export class SignalComponent {
randomNumber = signal(Math.random());
regenerate() {
this.randomNumber.set(Math.random());
}
}
Using Try/Catch in Angular
When Try/Catch Makes Sense
Angular’s error handler catches most errors, but sometimes you need local control:
export class DataProcessor {
async processData(data: any[]): Promise<ProcessedData> {
try {
// Risky transformation that could throw an error
const processed = await this.transform(data);
return processed;
} catch (error) {
// Log but don't crash
console.error('Processing failed, using defaults:', error);
// Return safe default
return this.getDefaultData();
}
}
}
Try/Catch vs ErrorHandler vs catchError
When to use each:
// Use try/catch for synchronous code with local recovery
try {
const result = riskyCalculation();
} catch (e) {
// Handle locally
}
// Use catchError for Observable streams
this.api.getData().pipe(
catchError(err => {
// Transform or recover
return of(defaultValue);
})
);
// Use ErrorHandler for unhandled errors
// (Catches everything else automatically)
Angular Error Handling Best Practices
1. Never Swallow Errors Silently
// BAD - Error disappears
this.api.getData().subscribe(
data => console.log(data),
error => {} // Silent failure!
);
// GOOD - Always handle or rethrow
this.api.getData().subscribe({
next: data => console.log(data),
error: error => {
this.notifier.showError('Failed to load data');
TrackJS.track(error);
}
});
2. Provide User-Friendly Messages
private getUserMessage(error: HttpErrorResponse): string {
const messages = {
0: 'Unable to connect. Please check your internet connection.',
400: 'Invalid request. Please check your input.',
401: 'Please log in to continue.',
403: 'You don\'t have permission to access this resource.',
404: 'The requested resource was not found.',
429: 'Too many requests. Please slow down.',
500: 'Server error. Our team has been notified.',
503: 'Service temporarily unavailable. Please try again later.'
};
return messages[error.status] || 'An unexpected error occurred.';
}
3. Add Context to Errors
TrackJS automatically adds tons of context (we call it Telemetry) to every error that get’s captured. .aside.tip
class ContextualError extends Error {
constructor(
message: string,
public context: any
) {
super(message);
this.name = 'ContextualError';
}
}
// Usage
throw new ContextualError('Failed to process order', {
orderId: order.id,
userId: user.id,
timestamp: new Date().toISOString(),
action: 'PROCESS_PAYMENT'
});
4. Implement Angular Error Boundaries
Angular doesn’t have a native “Error Boundary” feature like React, but it’s pretty easy to create one ourselves.
@Component({
selector: 'app-error-boundary',
template: `
<ng-content *ngIf="!hasError"></ng-content>
<div *ngIf="hasError" class="error-fallback">
<h2>Something went wrong</h2>
<button (click)="reset()">Try Again</button>
</div>
`
})
export class ErrorBoundaryComponent implements ErrorHandler {
hasError = false;
handleError(error: Error): void {
console.error('Error boundary caught:', error);
this.hasError = true;
}
reset(): void {
this.hasError = false;
}
}
Testing Error Handlers Effectively
You’re never really sure it works until you’ve seen it fail. We need to test our error handling with real errors to make sure it ..uh… handles them.
describe('GlobalErrorHandler', () => {
let handler: GlobalErrorHandler;
let mockNotifier: jasmine.SpyObj<NotificationService>;
beforeEach(() => {
const notifierSpy = jasmine.createSpyObj('NotificationService', ['showError']);
// Mock TrackJS for testing
(window as any).TrackJS = {
track: jasmine.createSpy('track')
};
TestBed.configureTestingModule({
providers: [
GlobalErrorHandler,
{ provide: NotificationService, useValue: notifierSpy }
]
});
handler = TestBed.inject(GlobalErrorHandler);
mockNotifier = TestBed.inject(NotificationService) as jasmine.SpyObj<NotificationService>;
});
afterEach(() => {
delete (window as any).TrackJS;
});
it('should log errors to TrackJS', () => {
const error = new Error('Test error');
handler.handleError(error);
expect((window as any).TrackJS.track).toHaveBeenCalledWith(
jasmine.objectContaining({
message: 'Test error'
})
);
});
it('should show user notification for HTTP errors', () => {
const httpError = new HttpErrorResponse({
error: 'Not found',
status: 404,
statusText: 'Not Found'
});
handler.handleError(httpError);
expect(mockNotifier.showError).toHaveBeenCalledWith(
'The requested resource was not found.'
);
});
it('should prevent error loops', () => {
const error = new Error('Loop test');
// Trigger many errors quickly
for (let i = 0; i < 20; i++) {
handler.handleError(error);
}
// Should stop after MAX_ERRORS (10)
expect((window as any).TrackJS.track).toHaveBeenCalledTimes(10);
});
});
Angular Error Monitoring in Production
Once you’ve tested everything and it’s time to go to Production, you’ll need to make sure you monitor what’s happening there for all the things we missed. We can’t possibly test every browser with every network and every possible sequence of user actions, right?!
That’s where an Error Monitoring Service like TrackJS comes in. Anytime your application fails in the hands of real users, we gather that error, normalize it, and show you the things that are causing problems the most.
Conclusion
Error handling in Angular isn’t glamorous, but it’s the difference between a professional application and a debugging nightmare. Start with a basic GlobalErrorHandler, add HTTP interceptors, and gradually build up your error handling infrastructure.
Remember, every error should be caught, logged, and handled appropriately, test your error handlers, and always monitor production. The patterns in this guide have saved me countless hours of debugging. Implement them, and you’ll sleep better knowing your Angular app can handle whatever errors come its way. Want to monitor your Angular errors in production? Try TrackJS free and see exactly what errors your users are experiencing.