How to log errors in Angular
Implementing robust error logging in Angular is critical for maintaining application stability and providing a professional user experience.
With over 25 years of experience in software development and as the creator of CoreUI, I’ve architected logging systems for enterprise-grade dashboards and complex web applications.
The most efficient and modern solution in Angular is to implement the global ErrorHandler interface, which allows you to intercept every unhandled exception in one centralized location.
This approach ensures that no error goes unnoticed while keeping your component logic clean and focused on business requirements.
Use Angular’s built-in ErrorHandler class to create a custom provider that intercepts all unhandled exceptions globally.
import { ErrorHandler, Injectable } from '@angular/core'
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
handleError(error: any): void {
const message = error.message ? error.message : error.toString()
console.error('Captured Global Error:', message)
}
}
1. Creating a Centralized Logging Service
Delegate the actual logging logic to a dedicated service. This service can handle formatting, environment checks, and communication with your backend API.
import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { environment } from '../environments/environment'
@Injectable({
providedIn: 'root'
})
export class LoggingService {
constructor(private http: HttpClient) {}
logError(error: any): void {
const errorLog = {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
url: window.location.href
}
if (!environment.production) {
console.error('[DEV] Error:', errorLog)
return
}
// In production, send to your remote logging endpoint
this.http.post('/api/logs', errorLog).subscribe({
error: () => console.error('Failed to send error log')
})
}
}
This separation of concerns allows you to easily swap your logging strategy — switching from a console log during development to a service like Sentry or a custom ELK stack in production. The environment check ensures verbose stack traces stay out of your production logs.
2. Implementing the Global Error Handler
Integrate LoggingService into GlobalErrorHandler. Because ErrorHandler is initialized before most other providers, use Injector to retrieve services manually and avoid circular dependency issues.
import { ErrorHandler, Injectable, Injector } from '@angular/core'
import { LoggingService } from './logging.service'
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
constructor(private injector: Injector) {}
handleError(error: any): void {
const logger = this.injector.get(LoggingService)
logger.logError(error)
console.error('Global Error:', error)
}
}
Using Injector is the canonical pattern here — injecting LoggingService directly into the constructor of ErrorHandler causes a circular dependency because Angular tries to instantiate both at the same time.
3. Registering the Custom Provider
Register your handler and wire up the HTTP client and interceptor in app.config.ts (standalone) or AppModule (NgModule-based).
Standalone (app.config.ts):
import { ApplicationConfig, ErrorHandler } from '@angular/core'
import { provideHttpClient, withInterceptors } from '@angular/common/http'
import { GlobalErrorHandler } from './global-error-handler'
import { errorInterceptor } from './error.interceptor'
export const appConfig: ApplicationConfig = {
providers: [
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
provideHttpClient(withInterceptors([errorInterceptor]))
]
}
NgModule (app.module.ts):
import { NgModule, ErrorHandler } from '@angular/core'
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'
import { GlobalErrorHandler } from './global-error-handler'
import { ErrorInterceptor } from './error.interceptor'
@NgModule({
imports: [HttpClientModule],
providers: [
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }
]
})
export class AppModule {}
provideHttpClient(withInterceptors([...])) is required for LoggingService to use HttpClient — omitting it causes a NullInjectorError at runtime.
4. Handling HTTP Errors with Interceptors
ErrorHandler catches runtime exceptions, but HTTP errors (404, 500, auth failures) need an HttpInterceptor for proper handling, retry logic, and contextual logging.
// error.interceptor.ts (functional interceptor — Angular 15+)
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http'
import { inject } from '@angular/core'
import { catchError, throwError } from 'rxjs'
import { LoggingService } from './logging.service'
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const logger = inject(LoggingService)
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
// Handle auth expiry — redirect to login
} else if (error.status >= 500) {
logger.logError(error)
}
return throwError(() => error)
})
)
}
Separating HTTP error handling from runtime error handling gives you fine-grained control: you can retry on 503, redirect on 401, and silently swallow 404s — none of which is appropriate in a generic ErrorHandler.
5. Displaying User Feedback with CoreUI Toast
Logging is only half the solution — you also need to notify the user without crashing the interface. Use CoreUI’s ToasterService for non-intrusive notifications.
First, add <c-toaster> to your root template so dynamic toasts have somewhere to render:
<!-- app.component.html -->
<router-outlet></router-outlet>
<c-toaster placement='top-end'></c-toaster>
Then trigger it from the error handler:
import { ErrorHandler, Injectable, Injector } from '@angular/core'
import { ToasterService, Toast } from '@coreui/angular'
import { LoggingService } from './logging.service'
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
constructor(private injector: Injector) {}
handleError(error: any): void {
const logger = this.injector.get(LoggingService)
const toaster = this.injector.get(ToasterService)
logger.logError(error)
const toast: Toast = {
color: 'danger',
title: 'Application Error',
body: 'Something went wrong. Our team has been notified.'
}
toaster.addToast(toast)
console.error('Global Error:', error)
}
}
Note the correct method is toaster.addToast(), not show(). Using CoreUI Toast ensures error notifications are visually consistent with the rest of your dashboard.
6. Environment-Specific Logging Levels
Production logs should contain only actionable information. Development logs should be verbose. Use Angular’s environment files to control this.
// logging.service.ts — extended with log levels
export enum LogLevel {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3
}
@Injectable({ providedIn: 'root' })
export class LoggingService {
private minLevel = environment.production ? LogLevel.Error : LogLevel.Debug
log(level: LogLevel, message: string, data?: any): void {
if (level < this.minLevel) return
const entry = { level: LogLevel[level], message, data, timestamp: new Date().toISOString() }
if (environment.production) {
this.http.post('/api/logs', entry).subscribe()
} else {
console[level >= LogLevel.Error ? 'error' : level >= LogLevel.Warn ? 'warn' : 'log'](entry)
}
}
logError(error: any): void {
this.log(LogLevel.Error, error.message || error.toString(), { stack: error.stack })
}
}
This pattern scales cleanly from a single developer to a team of 50 — junior devs get verbose console output locally, and production only ships what your monitoring service needs. For a complete production observability setup, see how to monitor Angular apps in production.
Best Practice Note:
Always sanitize error logs before sending them to a remote endpoint — strip authentication tokens, passwords, and personally identifiable information from stack traces and request payloads.
Use Injector inside ErrorHandler to avoid circular dependencies; never inject services directly into the constructor.
Pair this setup with a dedicated monitoring service for full production visibility — see how to use Sentry with Angular for the next step.



