Updated on
When an Angular HTTP request fails, HttpClient hands the failure to the error callback of subscribe as an HttpErrorResponse. Its status property carries the code the server returned, and everything else is a decision about where to send the reader: a 404 to a not-found page, a 500 to an error page, anything else to a message in place.
Here we build the 500 page, put the branching in one functional interceptor, and register it once so that every request the application makes runs through it. The owner details page follows, and it needs no error handling of its own, which is the point of moving the branching out of the components.
For the complete navigation and all the basic instructions of the Angular series, check out: Introduction of the Angular series.
For the previous part check out: Angular Lazy Loading: Load Routes on Demand, which put the owner feature behind a lazy route and fetched the owner list. This article handles what happens when that request fails.
How Do We Show a 404 or 500 Error Page in Angular?
An Angular error page is an ordinary component on an ordinary route. There is no special page type for failures: /404 and /500 are routes like /home, and the error handling navigates to them.
Two routes do the work. A path of 404 renders a not-found component, a path of 500 renders an internal-server component, and a wildcard route redirects anything unmatched to /404.
The wildcard has to be last. The router takes the first route whose path matches, so a ** entry placed above others swallows everything defined below it.
Navigating away is not the only option, and it is worth choosing deliberately. A redirect replaces whatever the reader had on screen and changes the address bar, which suits a failure they cannot recover from.
A message rendered in place suits the opposite case, where the request can simply be retried and the rest of the page is still useful.
The 404 page already exists from the routing part of this series, so the one we are missing is the 500. In the error-pages folder, we are going to create a new component by typing the Angular CLI command:
ng g component error-pages/internal-server --skip-tests
Then, let’s modify the internal-server.ts file:
import { Component } from '@angular/core';
@Component({
selector: 'app-internal-server',
styleUrl: './internal-server.css',
templateUrl: './internal-server.html',
})
export class InternalServer {
errorMessage = '500 SERVER ERROR, CONTACT ADMINISTRATOR!!!!';
}
The component holds one message and nothing else. The CLI generates it standalone, so no module has to declare it anywhere.
Then, let’s modify the internal-server.html file:
<p>{{errorMessage}}</p>
Additionally, we are going to modify the internal-server.css file:
p {
font-weight: bold;
font-size: 50px;
text-align: center;
color: #c72d2d;
}
Finally, let’s modify the routes array in the app.routes.ts file:
import { Routes } from '@angular/router';
import { Home } from './home/home';
import { NotFound } from './error-pages/not-found/not-found';
import { InternalServer } from './error-pages/internal-server/internal-server';
export const routes: Routes = [
{ path: 'home', component: Home },
{ path: 'owner', loadChildren: () => import('./owner/owner.routes').then(m => m.routes) },
{ path: '404', component: NotFound },
{ path: '500', component: InternalServer },
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: '**', redirectTo: '/404' }
];
Both error pages are ordinary routed components, reached the same way /home is. Anything the router cannot match, a stray /nope/whatever included, ends up on /404 through that last entry.
That’s it. We have created our component, and it is time to send failing requests to it.
How Do We Handle HTTP Errors in Angular?
HttpClient reports a failure as an HttpErrorResponse delivered to the error callback of subscribe, not as a thrown exception we can catch.
Its status property is what we branch on. A status of 0 means the request never reached the server, so there is nothing useful in the body. Any other value is the code the server sent back.
Two more properties carry the detail. error holds the parsed response body when there was one, and statusText holds the reason phrase. They are not the same type, which is why assigning either straight into a string field is a mistake.
Where the branching lives is the real decision. A service that every component injects and calls from its own error callback works, but it has to be wired in at each call site.
That is one forgotten subscribe away from a failure nobody handles, which is the problem an interceptor removes.
The diagram below follows one failed request from the component call to the page the user ends up on.
In the shared/interceptors folder, we are going to create a new interceptor and name it error:
ng g interceptor shared/interceptors/error --skip-tests
The CLI writes a functional interceptor by default and appends the type to the file name, so we get error-interceptor.ts holding a constant called errorInterceptor. Let’s modify it:
import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { EMPTY, catchError } from 'rxjs';
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router);
const createErrorMessage = (error: HttpErrorResponse): string =>
error.error ? error.error : error.statusText;
const handle500Error = () => {
router.navigate(['/500']);
};
const handle404Error = () => {
router.navigate(['/404']);
};
const handleOtherError = (error: HttpErrorResponse) => {
createErrorMessage(error); // this will be fixed later
};
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 500) {
handle500Error();
}
else if (error.status === 404) {
handle404Error();
}
else {
handleOtherError(error);
}
return EMPTY;
})
);
};
An interceptor is a plain function of the request and the next handler, which is the form Angular’s HTTP interceptors guide asks for: “Our recommendation is to use functional interceptors because they have more predictable behavior, especially in complex setups.” We take the Router with inject(), because a function has no constructor to inject into, pass the request on with next(req), and let catchError see whatever comes back as a failure.
Inside catchError we check the status code and call the matching helper, exactly as a hand-called service would have: a 500 navigates to /500, a 404 to /404, and anything else goes to handleOtherError, which is not finished yet.
createErrorMessage builds the text for that unfinished case: the response body when there is one, the status text otherwise. It is worth knowing what it holds before rendering it anywhere, because error.error is the parsed response body rather than a string. The modal that shows the message is built in part 12 and wired into this interceptor in part 13.
The last line is the one worth pausing on. EMPTY emits nothing and completes immediately, so a failed request never produces a value: the component’s next callback does not run, and no component needs an error callback to keep the failure quiet.
One registration puts the interceptor in front of every request. Let’s modify the app.config.ts file:
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { errorInterceptor } from './shared/interceptors/error-interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
provideHttpClient(withInterceptors([errorInterceptor]))
]
};
withInterceptors() takes the interceptor functions to run, in order, and provideHttpClient() is the provider we already added in part 9. Nothing else in the application changes.
That is the whole difference from a hand-called service. The owner list from part 10 subscribes exactly as it did:
private getAllOwners = () => {
const apiAddress: string = 'api/owner';
this.repository.getOwners(apiAddress)
.subscribe({
next: (own: Owner[]) => this.owners.set(own)
})
}
There is no error callback here and no error service to inject. The handling runs before the component ever sees the response, and every component we add in the parts that follow inherits it.
We can try it out by changing the code in the server’s GetAllOwners method. As the very first line we can add return NotFound(); or return StatusCode(500, "Some message");, and we are going to be redirected to the right error page.
Preparation for the Owner-Details Component
Let’s continue by creating the owner-details component:
ng g component owner/owner-details --skip-tests
To enable routing to this component, we need to modify the owner.routes.ts file:
import { Routes } from '@angular/router';
import { OwnerList } from './owner-list/owner-list';
import { OwnerDetails } from './owner-details/owner-details';
export const routes: Routes = [
{ path: 'list', component: OwnerList },
{ path: 'details/:id', component: OwnerDetails }
];
As you can see, the new path has the id parameter. So when we click on the Details button, we are going to pass this id to our route and fetch the owner with that exact id in the OwnerDetails component.
But, in order to be able to do that, we need to add a new interface to the _interfaces folder:
export interface Account {
id: string;
dateCreated: Date;
accountType: string;
ownerId?: string;
}
And modify the Owner interface:
import { Account } from './account.model';
export interface Owner {
id: string;
name: string;
dateOfBirth: Date;
address: string;
accounts?: Account[];
}
By using a question mark, we are making our property optional. That question mark comes back later in this article, when the template asks how many accounts an owner has.
To continue, let’s change the owner-list.html file:
<td><button type="button" id="details" class="btn btn-primary" (click)="getOwnerDetails(owner.id)">Details</button></td>
On a click event, we call the getOwnerDetails function and pass the owner’s id as a parameter. So we need to handle that click event in our owner-list.ts file.
First, let’s add an import statement:
import { Router } from '@angular/router';
Then, we inject the router beside the repository:
private router = inject(Router);
And add the getOwnerDetails(id) function:
public getOwnerDetails = (id: string) => {
const detailsUrl: string = `/owner/details/${id}`;
this.router.navigate([detailsUrl]);
}
We create a URI for our details component with the id parameter and then call the navigate function to navigate to that component. The id parameter carries its type because a strict project compiles with noImplicitAny, and an untyped parameter fails the build.
Finally, let’s just add one more function to fetch a single owner inside the owner-repository.service.ts file:
public getOwner(route: string) {
return this.http.get<Owner>(this.createCompleteRoute(route, this.envUrl.urlAddress));
}
Implementation of the Owner-Details Component
We have all the code to support the owner-details component. Now it is time to implement the logic inside that component.
Firstly, let’s modify the owner-details.ts file:
import { DatePipe } from '@angular/common';
import { Component, OnInit, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Owner } from '../../_interfaces/owner.model';
import { OwnerRepositoryService } from '../../shared/services/owner-repository.service';
@Component({
imports: [DatePipe],
selector: 'app-owner-details',
styleUrl: './owner-details.css',
templateUrl: './owner-details.html',
})
export class OwnerDetails implements OnInit {
owner = signal<Owner | undefined>(undefined);
private repository = inject(OwnerRepositoryService);
private activeRoute = inject(ActivatedRoute);
ngOnInit() {
this.getOwnerDetails()
}
getOwnerDetails = () => {
const id: string = this.activeRoute.snapshot.params['id'];
const apiUrl: string = `api/owner/${id}/account`;
this.repository.getOwner(apiUrl)
.subscribe({
next: (own: Owner) => this.owner.set(own)
})
}
}
It is pretty much the same logic as in the owner-list.ts file, except now we have ActivatedRoute imported, because we have to read our id from the route.
The fetched owner lives in a signal, the shape part 10 introduced, and set fills it in when the response arrives. Its type is Owner | undefined, because until then the component holds nothing at all.
All we have to do is to modify the owner-details.html file:
<div class="card card-body bg-light mb-2 mt-2">
<div class="row">
<div class="col-md-3">
<strong>Owner name:</strong>
</div>
<div class="col-md-3">
{{owner()?.name}}
</div>
</div>
<div class="row">
<div class="col-md-3">
<strong>Date of birth:</strong>
</div>
<div class="col-md-3">
{{owner()?.dateOfBirth | date: 'dd/MM/yyyy'}}
</div>
</div>
@if ((owner()?.accounts?.length ?? 0) <= 2) {
<div class="row">
<div class="col-md-3">
<strong>Type of user:</strong>
</div>
<div class="col-md-3">
<span class="text-success">Beginner user.</span>
</div>
</div>
} @else {
<div class="row">
<div class="col-md-3">
<strong>Type of user:</strong>
</div>
<div class="col-md-3">
<span class="text-info">Advanced user.</span>
</div>
</div>
}
</div>
<div class="row">
<div class="col-md-12">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Account type</th>
<th>Date created</th>
</tr>
</thead>
<tbody>
@for (account of owner()?.accounts; track account.id) {
<tr>
<td>{{account?.accountType}}</td>
<td>{{account?.dateCreated | date: 'dd/MM/yyyy'}}</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
Here, we display the owner entity with all the required data, and if the owner has more than two accounts, we show a different block for the type of user.
The ?? 0 in the @if is doing real work. owner() is undefined until the response lands, and accounts is optional on the interface we wrote a moment ago, so owner()?.accounts?.length is a number or undefined. Angular type-checks templates in a strict project and refuses to compare that with 2: without the ?? 0 the build stops with TS2532: Object is possibly 'undefined', and it stops in exactly the same way if we write owner()?.accounts.length and leave the second question mark out.
Finally, we display all the accounts related to this owner:
We can also take a look at the advanced user:
How Do We Handle Errors Globally in Angular?
Angular documents two places for error handling that is not written per request.
A functional HTTP interceptor sees every request HttpClient makes. It is a plain function taking the request and a next handler, registered once through provideHttpClient(withInterceptors([errorInterceptor])), and it can inspect the failure, navigate, and decide whether anything downstream ever sees it. Angular recommends this form over the class-based one because its ordering is predictable.
ErrorHandler is the other, and it works differently. It is the injection token Angular routes uncaught errors through, so it sees failures from anywhere in the application rather than only from HTTP, and its default implementation prints them to the console. Replacing that default takes one provider.
The two answer different questions. The interceptor knows about status codes and can decide where to send the reader. ErrorHandler is the last thing between an unexpected error and a silent failure, which makes it the right home for logging.
| Mechanism | What it catches | How it is registered | Reach for it when |
|---|---|---|---|
subscribe({ error: ... }) | one failed request, at that call site | nothing to register | only this component needs to react to this failure |
Functional interceptor (HttpInterceptorFn) | every failure from HttpClient | withInterceptors() inside provideHttpClient() | one place should map status codes to pages and messages |
ErrorHandler | uncaught errors from anywhere in the application, not only HTTP | {provide: ErrorHandler, useClass: MyErrorHandler} | logging and reporting, as the last line before a silent failure |
Class-based HttpInterceptor | same as the functional interceptor | HTTP_INTERCEPTORS plus withInterceptorsFromDi() | existing code only. Angular recommends the functional form because its ordering is predictable |
@error block inside @defer | a lazily loaded chunk that failed to arrive | in the template | showing fallback markup when a deferred view cannot load |
Angular’s API reference gives ErrorHandler one line: “Provides a hook for centralized exception handling.” Either way, our interceptor only reads what the server decided to send. On the other side of the wire, our ASP.NET Core guides cover how the API decides what to return when a request fails and IExceptionHandler, the current way to do it in .NET.
Conclusion
The error handling in this application now lives in one function, and every request the client makes passes through it. A component that forgets to handle a failure has stopped being a category of bug here.
In this post we have learned:
- What an
HttpErrorResponsecarries, and which of its properties to branch on - How to build 404 and 500 pages as ordinary routed components, and why the wildcard route stays last
- How to put the branching in one functional interceptor instead of repeating it in every component
- How to read a route parameter with
ActivatedRouteand render a single owner with their accounts - Where a functional interceptor and
ErrorHandlerfit, and what each one catches
In part 12: Angular @Input and @Output Decorators and Directives, we split the details page into child components and build the modal this article’s other errors need. That modal is what handleOtherError is waiting for: part 12 builds the component, and part 13 wires it into the interceptor and takes the comment out.
If you would rather build these screens with a component library, we cover the same error and details pages built with Angular Material.
Tested with Angular 22.1.5, Angular CLI 22.1.7, ngx-bootstrap 22.0.0, Bootstrap 5.3.8, TypeScript 6.0.3 and Node 24.




thank you. Worked well
Hi, In the owner-details.component.ts file at
I was getting the error “No overload matches this call.”
I changed
to match the git repo…
Yeah. Somehow that stayed unsaved, I fixed that once before. Very strange. Now, I’ve fixed it again, you can just clear the cache and you will find the changed code in the article. As you can I’ve even added that new function inside the repo file to fetch a single owner, and for some reason, the result of the owner array stayed in the snippet for the owner-details.ts file. Thank you very much for this suggestion.
No worries. I think it’s time for me to “shut it down” for the eve. as me ‘ole pops would say.
Hi,
I am using latest version on angular and trying to run the downloaded code but it shows following error:
core.js:6185 ERROR TypeError: Cannot read property ‘length’ of undefined
at OwnerDetailsComponent_Template (owner-details.component.html:18)
at executeTemplate (core.js:11949)
at refreshView (core.js:11796)
at refreshComponent (core.js:13229)
at refreshChildComponents (core.js:11527)
at refreshView (core.js:11848)
at refreshDynamicEmbeddedViews (core.js:13154)
at refreshView (core.js:11819)
at refreshComponent (core.js:13229)
at refreshChildComponents (core.js:11527)
The owner listing is working fine but no data is displayed on details page. Can you please guide me on this?
I do not want to bypass this error. When I check using console.warn() it shows the values:
(2) [{…}, {…}]
0:
AccountType: “Domestic”
DateCreated: “1996-02-15T00:00:00”
Id: “356a5a9b-64bf-4de0-bc84-5395a1fdc9c4”
OwnerId: “261e1685-cf26-494c-b17c-3546e65f5620”
__proto__: Object
1:
AccountType: “Foreign”
DateCreated: “1996-02-16T00:00:00”
Id: “c6066eb0-53ca-43e1-97aa-3c2169eec659”
OwnerId: “261e1685-cf26-494c-b17c-3546e65f5620”
__proto__: Object
length: 2
Hello, could you please look at the Dele Udma’s comment, maybe it can help you.
Hi Marinko,
Thanks for the response.
I am able to get rid of the error by doing a workaround as suggested by few users but not able to identify why it is shown as undefined.
Eventually, it doesn’t display any data on details page but the data is returned by the service and in the owner object (as mentioned above). I am trying these for couple of days without any success. Let me know if details are required.
https://github.com/rajeevjain1981/AccountOwnerExample
I will try it with Angular 12. But, even if you use these workarounds, you should see the data on the details page. These just help not to get an error until the accounts array gets populated. I am not sure why you cand see any data displayed, but as I said, I will check that out.
I’ve just treid it and with this statement:
it works normally, I can see the details of the user (their accounts). Also, I would like to ask you, how did you fix the JqueryUI problem, it seems that Angular 12 has an issue with it during the installation.
I followed the instructions given step by step and it all seems to be working fine. As data was shown correctly on List page and there was no error.
Is there anything I can do to check and ensure that Jqueryui is working fine.
No no, if it is working, then ok. I was just trying to do it as fastest as possible, to try to test your error, so maybe I meesed something up. Thanks.
Thanks Marinko.
You have been very cooperating and helpful. I have learnt .net Core WebAPI using your book and it worked well. Now I am learning how to consume the APIs in Angular.
I have a doubt that it has something to do with my local environmental settings only but not able to figure out the issue.
Let me know if there is a way to figure out the issue at my end.
Hi, I need help, I am using angular 11, I have been stock on part11 for the past week. I have error on this line after compilation.
Error: src/app/owner/owner-details/owner-details.component.html:18:45 – error TS2532: Object is possibly ‘undefined’.
Please compare your source code to ours. This is the best way to find what is different in your code.
I found out I could proceed without any errors when this setting “strictTemplates”: in the tsconfig.json is set to false. This sets typescript into a non-strict mode. I am progressing to the next part while I am still trying to find if there is an alternative code line to avoid the error in the strict mode. Thank you for your prompt response, I really appreciate it.
Thank you for your prompt response, I really appreciate it. I found out I could proceed without any errors when this setting “strictTemplates”: in the tsconfig.json is set to false. This sets typescript into a non-strict mode. I am progressing to the next part while I am still trying to find if there is an alternative code line to avoid the error in the strict mode.
I faced the same issue. I replaced the code with the below line and it worked
<div class="row" *ngIf='owner !== undefined && owner.accounts !== undefined &&owner.accounts.length
I faced the same issue. I replaced the code with the below line and it worked
Marinko I have no words to say you …. a big thanks … Very helpful resource you are sharing ….
Thank you very much for your feedback. Best regards.
Hi! Is there any tutorial to learn how to deal with many to many relationship with core & angular? (like User-Role) The way Core structure this relationships (with UserRole intermediate class) on Model it’s giving me so many problems when traying to show data on browser.
Hello Marinko,
is there backend tutorial capatible with that angular tutorial?
Regards,
Maciej
Hello Maciek. Yes there is. Please visit http://34.65.74.140/net-core-series/ . This is complete ASP.NET Core Web API series which we used as a server side for this tutorial. You can find that link even on the starting page for this tutorial as well: http://34.65.74.140/angular-series/ . Best regards.
thank you
Marinko Hi, I have a question.
When the API throws an error, in owner-list.component.ts (line 28 and 29) “this” object is not a OwnerListComponent anymore. (It’s a SafeSubscriber.)
Because of that this.errorHandle is undefined and error message from server never shows in the internal-server.component.html. What is the solution for this problem?
Hello Gkcn. Thank you very much for reading our post. Well if I understood you correctly, you have thrown an exception from the server (in GetAllOwners method) but in the client side, you have a problem. If that is the problem, I would really like more info from you because I have tried all (right now) to break the app and I couldn’t do it.
So this is the server part: https://uploads.disquscdn.com/images/a6ba003f991032192882cf42a2beba881c994fb87dde2a82fb9149a1ad3dc1d3.png
and this is the error object in line 28: https://uploads.disquscdn.com/images/e55ebcc284ba52e1636c1f8984a7c07434f47974beeb3ea376db9b2399fb2482.png
and this is the “this” object in line 29: https://uploads.disquscdn.com/images/fc15650ca03bd7e067b671a2d73b6877b183b36fb84f5be450658354cc82452f.png
As you can see, all is working as it supposed to do. You can share the code with me or to download our source code and to check for differences, because I can’t help you more right now, but I would like for sure. So if you can share some additional data, it would be great.
All the best.
Thank you for the fast response. 🙂
I figured out the problem. We shoul https://uploads.disquscdn.com/images/6c84244ff022642ca4e9806be5cd406fc162d3b4d1b9f51fa4e142be57dc1a1e.png d inject the ErrorHandleService to InternalServerComponent. And after that, on ngOnInit we should set the message to InternalServerComponent.ErrorMessage.
After doing these steps we will see the exception message in the 500 page. It’s strange that I still see the “this” as SafeSubscriber. But HandleError method still works. I can’t figured out.
I changed the error handling mechanism by returning json object when the exception occurs. This is why 500 page is little bit different.
Thank you for the fast response. 🙂 I figured out the problem. We should inject the ErrorHandleService to InternalServerComponent. And after that, on ngOnInit we should set the message to InternalServerComponent.ErrorMessage.
After doing these steps we will see the exception message in the 500 page. It’s strange that I still see the “this” as SafeSubscriber. But HandleError method still works. I can’t figured out.
I changed the error handling mechanism by returning json object when the exception occurs. This is why 500 page is little bit different.
These are the steps.
https://uploads.disquscdn.com/images/6c84244ff022642ca4e9806be5cd406fc162d3b4d1b9f51fa4e142be57dc1a1e.png
Hi mate. I am really glad that you solved it out, but I really don’t know why you had your problem at all. I believe that the problem is not related to the ErrorHandlerService but to the OwnerList component. But why, I am not sure at all. Either way, well done, your comments can help someone for sure if they find the same problem. Thank you for that.
These are the steps.