Error Handling in Angular

By Syncfusion®, Inc. · Last updated

Finding and handling errors: runtime, dependency injection and routing errors, ErrorHandler and a custom error handler, fallback UI, error logging, debugging and Angular DevTools.

Introduction to Angular Errors

An error is a problem that stops some code from finishing its work. In an Angular app, errors come from three places:

  • Your code: for example, reading a property of undefined.
  • Angular: for example, a missing provider or a route that does not exist.
  • The outside world: for example, a server that does not answer. You handled these HTTP errors in the API Integration lesson.

Good error handling has two goals: the user sees a clear message instead of a broken page, and the developer gets enough information to fix the problem.

Real-World Example

On a checkout page, a product from the server has no price. The template reads product.price.amount, and the page stops rendering. Without error handling, the customer sees half a page.

With error handling, a global error handler records the error and the page shows "Something went wrong. Please reload the page." The team sees the error in their log and fixes the data.

Errorin a click or templateErrorHandlerhandleError()Logfor developersFallback UIfor usersOne handler receives every uncaught error, logs it and shows a clear message.

Global error handling in one place.

Angular errors have a code, such as NG0201. Search for the code at angular.dev/errors to read what it means and how to fix it.

Runtime Errors

A runtime error happens while the app is running, not when it is built. The most common one is reading a property of null or undefined:

user.nameuser?.name ?? 'Guest'user = nullread .nameTypeErrorRendering stopshalf a pageuser = null?. stops safelyundefined?? gives a defaultHello, Guest

Optional chaining and ?? turn a crash into a default value.

TypeError: Cannot read properties of undefined (reading 'name')

When this happens in a template, Angular stops rendering that part of the page.

Ways to prevent it:

  • Optional chaining ?. stops and returns undefined when the value before it is null or undefined: user?.name.
  • ?? gives a default value: user?.name ?? 'Guest'.
  • @if renders the part only when the value exists.
  • try / catch in TypeScript catches an error that a function throws, so the rest of the code can continue.

Preventing runtime errors

import { Component } from '@angular/core';

interface User {
  name: string;
  address?: { city: string };
}

@Component({
  selector: 'app-root',
  template: `
    <p>Name: {{ user?.name ?? 'Guest' }}</p>
    <p>City: {{ user?.address?.city ?? 'not set' }}</p>
    @if (user) {
      <p>Welcome back, {{ user.name }}!</p>
    }
    <p>Saved settings: {{ settingsText }}</p>
  `,
})
export class App {
  user: User | null = { name: 'Asha' };
  settingsText = this.readSettings('{ broken json');

  readSettings(text: string) {
    try {
      return JSON.parse(text).theme;
    } catch {
      return 'could not read the settings, using the defaults';
    }
  }
}

user.address does not exist, but ?. prevents the error. JSON.parse() throws an error for broken text, and catch returns a default message instead.

Dependency Injection Errors

A dependency injection error happens when a class asks for something that Angular cannot provide. The most common one:

NG0201: No provider for AnalyticsService found in NodeInjector
NullInjectorError: No provider for AnalyticsService!

The next block fails on purpose. AnalyticsService has @Injectable() without providedIn, and no providers array lists it.

A missing provider

import { Component, Injectable, inject } from '@angular/core';

@Injectable()
export class AnalyticsService {}

@Component({
  selector: 'app-root',
  template: `<p>Dashboard</p>`,
})
export class App {
  analytics = inject(AnalyticsService);
}

How to fix it:

  • Add providedIn: 'root' to @Injectable(), or list the service in a providers array.
  • Check the import. Two classes with the same name in different files are two different tokens.
  • If the dependency is optional, use inject(AnalyticsService, { optional: true }). It returns null instead of an error.

A block that fails shows the real Angular error in its output. Read the first line: it names the error code and the missing provider.

Another DI error is NG0203: inject() called outside an injection context, for example inside a click method. Call inject() only in a field or the constructor.

Routing Errors

The router can fail in these ways:

ProblemWhat happensFix
The URL matches no routeError NG04002: Cannot match any routes, and the page stays the sameAdd a wildcard route '**' with a "Page not found" component
routerLink has a wrong pathSame as aboveCheck the path, and use the wildcard route
A guard throws an errorThe navigation is cancelledReturn true or a UrlTree from guards, never throw
A lazy-loaded file cannot downloadThe navigation fails, for example on a bad connectionShow a message and let the user try again

router.navigate() returns a promise. When the navigation fails, the promise is rejected, so you can handle it with .catch():

Handling a failed navigation

import { Component, inject, signal } from '@angular/core';
import { Router, RouterOutlet, Routes } from '@angular/router';

@Component({ selector: 'app-home', template: `<p>🏠 Home page</p>` })
export class Home {}

export const routes: Routes = [{ path: '', component: Home }];

@Component({
  selector: 'app-root',
  imports: [RouterOutlet],
  template: `
    <button (click)="open('/reports')">Open reports</button>
    @if (message()) {
      <p>⚠ {{ message() }}</p>
    }
    <router-outlet />
  `,
})
export class App {
  private router = inject(Router);
  message = signal('');

  open(url: string) {
    this.router.navigateByUrl(url).catch(() => {
      this.message.set(`The page ${url} does not exist.`);
    });
  }
}

There is no reports route, so the navigation fails and the message appears. In a real app, the wildcard route usually handles unknown URLs.

Using try, catch, and finally

try, catch and finally are JavaScript statements for handling an error inside one function:

  • try { ... } contains the code that can fail.
  • catch (error) { ... } runs only when the code in try throws an error. The error variable contains the error.
  • finally { ... } runs always, after try or after catch. Use it for work that must happen in both cases, such as hiding a loading message.

No errorError in trytryall code runscatchskippedfinallyrunstrystops at the errorcatch (error)handles itfinallyruns

finally always runs, with or without an error.

try {
  // code that can fail
} catch (error) {
  // runs only if try throws
} finally {
  // runs in every case
}
  • When try throws, the rest of the try block is skipped, and the code continues in catch.
  • After catch, the function continues normally. The error does not reach the global error handler.
  • If you do not need the error, write catch { ... } without (error).
  • catch and finally are both optional, but you need at least one of them.

The next example reads saved settings. Click Load valid settings and Load broken settings: the broken text makes JSON.parse() throw, catch shows a message, and finally hides "Loading..." in both cases.

try, catch and finally

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <button (click)="load('{ &quot;theme&quot;: &quot;dark&quot; }')">Load valid settings</button>
    <button (click)="load('{ broken')">Load broken settings</button>
    @if (loading()) {
      <p>Loading...</p>
    }
    <p>Theme: {{ theme() }}</p>
    @if (error()) {
      <p>⚠ {{ error() }}</p>
    }
    <p>Steps: {{ steps().join(' → ') }}</p>
  `,
})
export class App {
  loading = signal(false);
  theme = signal('light');
  error = signal('');
  steps = signal<string[]>([]);

  load(text: string) {
    this.loading.set(true);
    this.error.set('');
    this.steps.set(['try']);
    try {
      const settings = JSON.parse(text); // throws for broken text
      this.theme.set(settings.theme);
      this.steps.update(list => [...list, 'parsed']);
    } catch (error) {
      this.error.set('The saved settings are broken. The default theme is used.');
      this.theme.set('light');
      this.steps.update(list => [...list, 'catch']);
    } finally {
      this.loading.set(false);
      this.steps.update(list => [...list, 'finally']);
    }
  }
}

After the valid settings, the steps are try → parsed → finally. After the broken settings, they are try → catch → finally: the parsed step is skipped.

try / catch catches only errors that happen while the try block runs. An error inside a subscribe() callback or a setTimeout() happens later, so it is not caught. For HTTP requests, use catchError() and finalize() from RxJS, which work the same way for Observables.

Global Error Handling

You cannot put try / catch around every line. Global error handling catches every error that your code does not catch, in one place.

Angular sends these errors to one service:

  • Errors in event handlers, such as (click)
  • Errors while rendering templates
  • Errors in lifecycle hooks and in subscribe() callbacks without an error function

ErrorHandler

ErrorHandler is the Angular service that receives all uncaught errors. Its method handleError(error) is called for each one. The default ErrorHandler writes the error to the browser console with console.error().

Open the browser console (F12, Console tab) and click the button below. The error appears in red, but the user sees nothing.

The default ErrorHandler writes to the console

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <button (click)="placeOrder()">Place order</button>
    <p>The page does not show the error. Look in the browser console.</p>
  `,
})
export class App {
  placeOrder() {
    throw new Error('Payment service is not configured');
  }
}

Custom Error Handler

You can replace the default service with your own class. It implements ErrorHandler and is registered with a provider in app.config.ts:

export const appConfig: ApplicationConfig = {
  providers: [{ provide: ErrorHandler, useClass: AppErrorHandler }],
};

This is the useClass provider from the Dependency Injection lesson: when Angular asks for ErrorHandler, it gets an AppErrorHandler.

A custom handler usually does three things:

  1. Writes the error to the console, so developers still see it.
  2. Sends it to a log (the next sections).
  3. Tells the app to show a fallback UI to the user.

A custom ErrorHandler with a fallback message

import { ApplicationConfig, Component, ErrorHandler, Injectable, inject, signal } from '@angular/core';

// Shared state: the current error message for the user
@Injectable({ providedIn: 'root' })
export class ErrorState {
  readonly message = signal('');
}

@Injectable()
export class AppErrorHandler implements ErrorHandler {
  private state = inject(ErrorState);

  handleError(error: unknown) {
    console.error('AppErrorHandler:', error); // 1. for developers
    this.state.message.set('Something went wrong. Please try again.'); // 3. for users
  }
}

export const appConfig: ApplicationConfig = {
  providers: [{ provide: ErrorHandler, useClass: AppErrorHandler }],
};

@Component({
  selector: 'app-root',
  template: `
    @if (errors.message(); as message) {
      <p class="banner">⚠ {{ message }} <button (click)="errors.message.set('')">Close</button></p>
    }
    <button (click)="placeOrder()">Place order</button>
  `,
  styles: `.banner { background: #fee2e2; color: #991b1b; padding: 6px 10px; border-radius: 6px; }`,
})
export class App {
  errors = inject(ErrorState);

  placeOrder() {
    throw new Error('Payment service is not configured');
  }
}

Click Place order. The error goes to AppErrorHandler, which shows a banner instead of failing silently.

Never show the technical error text, such as a stack trace, to users. It confuses them and can reveal details about your code. Show a short, clear message, and keep the details in the log.

Fallback UI

A fallback UI is what the user sees when something fails, instead of an empty or broken page. Common examples:

Product: Wireless Headphones, $95Description and photos⚠ Reviews could not be loaded[ Try again ]Only the failed part shows a message. The rest of the page keeps working.

A fallback inside one component keeps the page usable.

  • An error banner at the top of the page, as above
  • An error message inside one component, while the rest of the page keeps working
  • A "Try again" button that repeats the failed action
  • A "Page not found" page for unknown routes

The next example shows a component-level fallback. Only the reviews section fails; the product information stays on the page.

A fallback inside one component

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-reviews',
  template: `
    <h4>Reviews</h4>
    @if (failed()) {
      <p>Reviews could not be loaded. <button (click)="load()">Try again</button></p>
    } @else {
      @for (review of reviews(); track $index) {
        <p>⭐ {{ review }}</p>
      }
    }
  `,
})
export class Reviews {
  reviews = signal<string[]>([]);
  failed = signal(false);
  private attempts = 0;

  constructor() {
    this.load();
  }

  load() {
    this.attempts++;
    try {
      // The first attempt fails, the second one works
      if (this.attempts === 1) {
        throw new Error('Reviews server timeout');
      }
      this.reviews.set(['Great sound!', 'Comfortable to wear.']);
      this.failed.set(false);
    } catch {
      this.failed.set(true);
    }
  }
}

@Component({
  selector: 'app-root',
  imports: [Reviews],
  template: `
    <h3>Wireless Headphones</h3>
    <p>Price: $95</p>
    <app-reviews />
  `,
})
export class App {}

Click Try again: the second attempt works and the reviews appear.

Error Logging

The console exists only in the user's browser. When a user has an error, the developers never see it. Error logging means collecting errors in a place where the team can read them.

Record useful details with each error:

  • The error message
  • The time
  • The URL of the page
  • What the user did, if known (for example, "clicked Place order")

The simplest way is a LoggingService that the error handler calls. In a real app, it sends each entry to a server with HttpClient, or to an error-monitoring service that your team uses.

An error log

import { ApplicationConfig, Component, ErrorHandler, Injectable, inject, signal } from '@angular/core';

export interface LogEntry {
  time: string;
  message: string;
  page: string;
}

@Injectable({ providedIn: 'root' })
export class LoggingService {
  readonly entries = signal<LogEntry[]>([]);

  logError(error: unknown) {
    const entry: LogEntry = {
      time: new Date().toLocaleTimeString(),
      message: error instanceof Error ? error.message : String(error),
      page: location.pathname,
    };
    this.entries.update(list => [...list, entry]);
    // In a real app: this.http.post('/api/logs', entry).subscribe();
  }
}

@Injectable()
export class AppErrorHandler implements ErrorHandler {
  private logger = inject(LoggingService);
  handleError(error: unknown) {
    console.error(error);
    this.logger.logError(error);
  }
}

export const appConfig: ApplicationConfig = {
  providers: [{ provide: ErrorHandler, useClass: AppErrorHandler }],
};

@Component({
  selector: 'app-root',
  template: `
    <button (click)="save()">Save profile</button>
    <button (click)="upload()">Upload photo</button>
    <h4>Error log ({{ logger.entries().length }})</h4>
    @for (entry of logger.entries(); track $index) {
      <p>{{ entry.time }} | {{ entry.message }}</p>
    }
  `,
})
export class App {
  logger = inject(LoggingService);

  save() {
    throw new Error('Profile service returned no data');
  }

  upload() {
    throw new Error('Photo is larger than 5 MB');
  }
}

error instanceof Error checks if the value is a real Error object. Code can throw any value, so the check makes the message safe to read.

Debugging Techniques

Debugging means finding the cause of an error. Useful techniques:

  • Read the full error message. It names the problem and often the component and the line.
  • Look up the NG code at angular.dev/errors.
  • console.log() prints values, so you can check what the code really receives.
  • The debugger; statement pauses the code at that line when the developer tools are open. Then you can read every variable and run the code step by step.
  • Breakpoints: in the Sources tab of the developer tools, click a line number to pause there, without changing the code.
  • The Network tab shows every HTTP request, its status code and its answer.
  • Development mode: ng serve builds the app with extra checks and clearer error messages. Production builds remove them.
calculateTotal(items: CartItem[]) {
  console.log('items:', items);   // check the input
  debugger;                       // pause here when the developer tools are open
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}

Remove console.log() and debugger; before you commit your code. They are for finding a problem, not for the finished app.

Angular DevTools

Angular DevTools is a free browser extension for Chrome and Firefox, made by the Angular team. After you install it, the developer tools get an Angular tab when you open an Angular app in development mode.

FeatureWhat it shows
ComponentsThe tree of all components on the page
PropertiesThe inputs, outputs, signals and fields of the selected component. You can change them to test
Injector treeWhich injector provides each service
Router treeAll routes of the app
ProfilerHow long each change detection run takes, and which components it checked

In the Console tab, you can also select an element in the Elements tab and run ng.getComponent($0) to read the component that renders it. This works only in development mode.

Error Handling Best Practices

  • Prevent errors first: use ?., ??, @if and TypeScript types.
  • Handle expected errors where they happen: HTTP errors with catchError() or an error callback, and broken input with try / catch.
  • Add a custom ErrorHandler for all other errors, so none of them is lost.
  • Show clear messages to users, and keep technical details in the log.
  • Keep the rest of the page working: show the fallback inside the failed component when you can.
  • Log errors to a server with the time, the page and the message.
  • Add a wildcard route for unknown URLs.
  • Test the error paths, for example with a server that is turned off.
Error typeWhere to handle it
A value can be missingTemplate: ?., ??, @if
A function can throwtry / catch in that function
An HTTP request failscatchError() or the error callback
An unknown URLThe wildcard route '**'
Everything elseA custom ErrorHandler

Common Mistakes

MistakeWhat you seeFix
user.name when user can be nullThe template stops renderingWrite user?.name or use @if (user)
A service without providedIn or a providerNG0201: No provider for ...Add providedIn: 'root' or list it in providers
An empty catch {} that hides errorsThe problem happens again and nobody knows whyLog the error, and show a message
Showing error.stack to usersUsers see confusing technical textShow a short message, log the details
A custom ErrorHandler that does not logDevelopers cannot see the errors any moreCall console.error() and the logging service
No wildcard routeUnknown URLs leave the old page on screenAdd { path: '**', component: NotFound }

Check Yourself

Exercises

What to Remember

  • Prevent runtime errors with ?., ?? and @if, and catch expected errors where they happen: try / catch for functions and catchError() for HTTP.
  • DI errors (NG0201) mean a missing provider; routing errors need a wildcard route and .catch() on navigate(). Look up every NG code at angular.dev/errors.
  • Replace ErrorHandler with your own class to catch all other errors: log them for developers and show a fallback UI for users. Debug with the browser developer tools and Angular DevTools.

Next: Performance Optimization, how to make your app load fast and update efficiently.