Error Handling & Logging
Every real app fails sometimes. The network drops on a train, an API returns 503 during a deployment, or a developer forgets that an optional value can be undefined. Users forgive an error that is explained clearly; they do not forgive a frozen screen. Your team, meanwhile, needs to know about errors that happen on users' phones, not discover them in a one-star review.
In this lesson you will handle errors at the right layer and build a global error handler with a small logging service that sends reports to your server.
Three layers of error handling
Think of errors in three groups, each with its own home:
| Kind of error | Example | Where to handle it |
|---|---|---|
| Expected, local | Form input is invalid, a search finds nothing, an API returns 404 | In the component or service that made the call |
| Expected, app-wide | Network down, session expired, server error | In HTTP interceptors (HTTP Interceptors: Auth, Errors & Retries) |
| Unexpected bugs | Cannot read properties of undefined | In a global ErrorHandler that logs and reports |
The golden rule: handle an error as close as possible to where you can do something useful about it, and let only truly unexpected errors reach the global handler.
Handling errors locally
When you call code that can fail, decide what the user should see. With async/await, use try/catch. With observables, use the error callback or the catchError operator. With resource and httpResource, the error is part of the resource's state, so the template can show it directly:
import { Component, resource } from '@angular/core';
@Component({
selector: 'app-holiday-list',
template: `
@if (holidays.error()) {
<p class="error">Could not load holidays. {{ holidays.error()?.message }}</p>
<button (click)="holidays.reload()">Try again</button>
} @else if (holidays.isLoading()) {
<p>Loading...</p>
} @else {
<p>{{ holidays.value() }}</p>
}
`,
})
export class HolidayList {
protected holidays = resource({
loader: async () => {
throw new Error('Server returned 503');
},
});
}Could not load holidays. Server returned 503 [Try again]
The resource's status() is 'error', and the error does not reach the global error handler, because you have handled it in the template. This is the behaviour you want for expected failures: a clear message and a way to retry.
What reaches the global ErrorHandler
Angular sends errors to the app's ErrorHandler when they are thrown from code that Angular itself calls: constructors, lifecycle hooks, template event handlers such as (click), and rendering. Errors in your own setTimeout callbacks or forgotten promises are caught by the browser instead. provideBrowserGlobalErrorListeners(), which new projects already have in app.config.ts, listens for the browser's error and unhandledrejection events and forwards those to the ErrorHandler too.
The default ErrorHandler only writes to the console. That is fine on your laptop, but on a user's phone in Jaipur nobody reads the console. Replace it with your own.
Step by step: a global handler with remote logging
- Create a
Loggerservice. It collects log entries in a small buffer and sends them in batches, so a burst of errors does not become a burst of requests.navigator.sendBeaconsends data even while the page is closing:
import { DOCUMENT, Service, inject } from '@angular/core';
import { Router } from '@angular/router';
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
export interface LogEntry {
level: LogLevel;
message: string;
url: string;
time: string;
ref?: string;
}
@Service()
export class Logger {
private router = inject(Router);
private buffer: LogEntry[] = [];
constructor() {
inject(DOCUMENT).defaultView?.addEventListener('pagehide', () => this.flush());
}
log(level: LogLevel, message: string, ref?: string) {
this.buffer.push({ level, message, url: this.router.url, time: new Date().toISOString(), ref });
if (level === 'error' || this.buffer.length >= 10) {
this.flush();
}
}
flush() {
if (this.buffer.length === 0) return;
navigator.sendBeacon('/api/logs', JSON.stringify(this.buffer));
this.buffer = [];
}
}- Create the handler. It implements the
ErrorHandlerinterface, gives each error a short reference code, logs it and shows a friendly message. The reference code lets support staff find the exact log entry when a user calls:
import { ErrorHandler, Service, inject } from '@angular/core';
@Service()
export class GlobalErrorHandler implements ErrorHandler {
private logger = inject(Logger);
private toasts = inject(Toasts);
handleError(error: unknown): void {
const ref = crypto.randomUUID().slice(0, 8).toUpperCase();
const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
this.logger.log('error', message, ref);
this.toasts.show(`Something went wrong. Reference: ${ref}`);
console.error(error);
}
}Toasts is a small service holding a signal list of messages, displayed by a toast component in the app shell. Keep console.error so developers still see the full error with its stack trace in DevTools.
- Register it in
app.config.ts.useExistingtells Angular to use the sharedGlobalErrorHandlerinstance whenever anything asks forErrorHandler:
import { ApplicationConfig, ErrorHandler, provideBrowserGlobalErrorListeners } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
{ provide: ErrorHandler, useExisting: GlobalErrorHandler },
],
};- Try it. A payslip page has a bug: its click handler reads
.namefrom a month that was never loaded. Clicking "Download payslip" sends this batch (time shortened) and shows the toast:
[{"level":"error","message":"TypeError: Cannot read properties of undefined (reading 'name')","url":"/","time":"T","ref":"3F9A1C2E"}]
Toast: Something went wrong. Reference: 3F9A1C2E- Build the server side. The
/api/logsendpoint stores entries with the app version and a timestamp. Many teams forward them to a monitoring service instead of writing their own; most such services offer an Angular SDK that plugs intoErrorHandlerin exactly the way you just did.
Log responsibly
- Never log secrets or personal data: no passwords, tokens, Aadhaar or PAN numbers, full phone numbers or salary figures. India's Digital Personal Data Protection Act, 2023 makes careless handling of personal data a legal problem, not only a technical one. Log IDs instead of names.
- Use levels.
debugandinfoare useful during development; in production, send onlywarnanderrorto the server, or you will drown in noise and pay for storage. - Upload source maps privately. Production bundles are minified, so stack traces show names like
e.a. Build with"sourceMap": { "scripts": true, "hidden": true }to create maps without linking them from the bundle, then give them to your monitoring tool, not to the public web server. - Do not create loops. If sending a log fails, do not log that failure through the same path.
Errors in tests
TestBed rethrows application errors by default, so a bug in a click handler fails the test instead of being swallowed. When a test specifically checks your error handler, turn that off:
TestBed.configureTestingModule({
providers: [{ provide: ErrorHandler, useExisting: GlobalErrorHandler }],
rethrowApplicationErrors: false,
});Common mistakes
- Catching an error and doing nothing. An empty
catch {}hides bugs. At least log it. - Showing technical messages such as
TypeError: undefinedto users. Show a friendly message and a reference code. - Sending every console message to the server. Filter by level and batch the requests.
- Logging personal data, which creates privacy and legal risk.
- Removing
provideBrowserGlobalErrorListeners()and then wondering why unhandled promise rejections never appear in your logs.
Key points
- Handle expected errors locally (try/catch,
catchError, resourceerror()state); app-wide HTTP errors belong in interceptors. - Angular routes errors from constructors, hooks, event handlers and rendering to
ErrorHandler;provideBrowserGlobalErrorListeners()adds browsererrorandunhandledrejectionevents. - Replace the default handler with
{ provide: ErrorHandler, useExisting: GlobalErrorHandler }to log remotely and show a friendly message. - Batch logs, use levels, send with
navigator.sendBeacon, and never log secrets or personal data. - Keep source maps hidden and private so production stack traces stay readable for your team.
Exercise
Extend the Logger so that in development it prints every entry to the console and sends nothing, while in production it sends only warn and error entries. Use Angular's isDevMode() function from @angular/core. Then add a warn call in a leave form when an employee requests more than 10 days at once.
Show solution
import { isDevMode } from '@angular/core';
@Service()
export class Logger {
private router = inject(Router);
private buffer: LogEntry[] = [];
private readonly dev = isDevMode();
log(level: LogLevel, message: string, ref?: string) {
const entry: LogEntry = { level, message, url: this.router.url, time: new Date().toISOString(), ref };
if (this.dev) {
console.log(`[${level}] ${message}`);
return;
}
if (level !== 'warn' && level !== 'error') {
return;
}
this.buffer.push(entry);
if (level === 'error' || this.buffer.length >= 10) {
this.flush();
}
}
// flush() as before
}
// In the leave form's submit method:
if (days > 10) {
this.logger.log('warn', `Long leave requested: ${days} days`);
}isDevMode() is true under ng serve and false in a production build. The warning logs only the number of days, not the employee's name, which keeps personal data out of the logs.
Quick Quiz
1. Where should a "search returned no results" situation be handled?
Expected, local situations get a clear message where they happen.
2. Which of these reaches Angular's ErrorHandler without extra setup?
Errors from code Angular calls, such as template event handlers, go to ErrorHandler.
3. What does provideBrowserGlobalErrorListeners() add?
It catches errors that happen outside Angular's own calls.
4. Which value is safe to include in a production log?
Never log secrets or personal data.
Learn Angular with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
