Introduction
Error handling is as much of an important topic as it is also hated, and even more so, overlooked. Of course, we all enjoy authoring cool features, fascinating animations and beautiful UIs, but not so much do we love writing a bunch of code whose only purpose is to save us when something goes wrong.
However, an important part of a developer's journey to maturity is realizing that errors are inescapable. A third-party library might contain a bug; a network request might fail; something might be wrong with the end user's machine. In all such scenarios - and more - we need to be able to meet these errors gracefully, and not allow our application to break because of simple scenarios that we are capable of anticipating. So, let's get started with first covering the basics!
Synchronous Error Handling
Synchronous errors are rare in Angular apps, since around 90% of accidents arise when making network calls and such; however, they might happen, and here are a couple of examples:
- Using a third-party library which raises an error in some circumstances (wrong format of data passed, etc)
- Using a third-party library which has a bug that causes an error to be thrown
- Using your own project's code that throws errors in some cases
- Using your own project's code that has a bug
Now, handling all of those is pretty simple:
- Examine the circumstances under which the error is thrown, and fix your usage of the library
- There isn't much we can do to the library code that has a bug, but we can report it to the library author, and, in our code, add a
try
/catch
statement to handle the error gracefully (or, if you have the time and motivation, please consider actually fixing the bug in the library and sending a PR) - Use the code in our codebase correctly so as not to get an error; or, if the error itself is raised wrongfully, change the underlying code
- Fix the bug if possible or find a workaround not to get the error, or actually find the root cause of the issue to avoid introducing another bug or increasing complexity: adding
try
/catch
to this code should be a last resort, as it can make the code appear logical, all the while it actually covers up for a bug. Consider adding a comment explaining the situation if you end up usingtry
/catch
after all
Now, we have discussed "local" errors and handling them; errors that happen in very particular situations and are handled in very particular ways. But what if we need a way to intercept errors and do something on a global scale? Let's talk about it.
Global Error Handling
Angular provides a way to handle errors globally, and it is done by implementing the ErrorHandler
class. This class has a single method, handleError
, which is called whenever an error occurs in the application. To use it, we need to create a new class that implements the ErrorHandler
, override the handleError
method, and then re-provide the ErrorHandler
in our application config to make it use our own implementation:
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
// this method will receive the error from anywhere in our app and handle it
handleError(error: unknown): void {
// log the error
console.error('An error occurred:', error);
// perform other error-handling operations, like showing toast messages,
// sending the error to analytics, and so on
}
}
And in the app.config.ts
file:
export const config = {
providers: [
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
],
};
And that's it! Now, any uncaught error in our application will pass through the handleError
method of our GlobalErrorHandler
class. We can easily check it by adding a error in some random component:
@Component({
selector: 'app-root',
template: `
<h1>Angular Error Handling</h1>
<button (click)="throwError()">Throw Error</button>
`,
})
export class AppComponent {
throwError() {
// test with the click of the button
throw new Error('This is a test error!');
}
}
When we click the button, we should see the error logged in the console, and any other operations we added in the handleError
method will be executed as well.
Now, the fascinating thing about ErrorHandler
is that it is not limited to just synchronous errors, but will also pass through async errors as well. We can see it by adding a simple fetch
in some random component:
@Component({
selector: 'app-root',
template: `
<h1>Angular Error Handling</h1>
<button (click)="fetchData()">Fetch Data</button>
`,
})
export class AppComponent {
fetchData() {
// test with the click of the button
fetch('http://some-wrong-api.com/wrong-endpoint');
}
}
When we click the button, we will again see the handleError
method in action. However, this is suitable for generic, global tasks, but async error handling often involves advanced and highly localized tasks, such as showing some default data, loading a different page depending on the context, and more.
Note:
ErrorHandler
will be triggered on any error within your app, for example, if an image failed to load, or some library threw and inconsequential error. So, filtering error types in thehandle
method is incredibly important!
Now, everything we covered here relates to errors happening inside our Angular app; but what if there are errors from an outside context? Maybe there's a third party script running from the index.html
file, or we could be using Angular Elements to incorporate our Angular components inside other applications and want to monitor errors that happen there. Until v20, it was not possible to catch those, but in v20, Angular is introducing a new config that allows us to catch errors that happen outside of Angular's context. We can simply add that command to our app.config.ts
file:
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
]
};
This config will redirect error handling from browser's window.onerror
and window.onunhandledrejection
to Angular's ErrorHandler
, which we covered previously.
So, let's now explore different approaches to async error handling in Angular.
HTTP Errors
We will start by discussing HTTP as it is, without involving concepts like signals and resources; we will discuss them further down this article.
Handling HTTP errors using RxJS capabilities
Simply put, an HTTP request in an Angular app, if we, of course, utilize the HttpClient
, is an Observable, so handling that sort of error would come down to error handling with RxJS. Let's start with the simplest example:
@Component({
selector: 'app-root',
template: `
<h1>Angular Error Handling</h1>
<button (click)="fetchData()">Fetch Data</button>
`,
})
export class AppComponent {
constructor(private http: HttpClient) {}
fetchData() {
this.http.get('http://some-wrong-api.com/wrong-endpoint').subscribe({
next: (data) => console.log(data),
error: (error) => console.error('Error fetching data:', error),
});
}
}
In this example, we are using just the subscribe
method of the Observable to handle the error. The subscribe
method can take not just one callback, but an Observer
interface, which is an object that can have 3 methods: next
, error
, and complete
.
next
: this method is called when the Observables sends a new value, in our case, the HTTP responseerror
: this method is called when the Observable encounters an error, in our case, when the HTTP request failscomplete
: this method is called when the Observable completes, which in our case is equivalent to HTTP success, so we don't have to implement it.
However, this is probably the worst way to handle HTTP errors within an Angular app, as we know there are better ways to work with Observables. For instance, if we want to show the data we receive via HTTP in the UI, we might use the async
pipe, at which point we won't have a subscribe
callback to handle the error, and would instead opt to displaying something in the UI.
This can be achieved via the catchError
operator; what it does is, when an error occurs, it invokes the callback we provide which must return a different Observable, that will "replace" the original Observable. This can be used to return some sort of "error object" that can be used to handle the situation in the UI. Here is the same example, but using catchError
:
import { catchError } from 'rxjs/operators';
import { of } from 'rxjs';
@Component({
selector: 'app-root',
template: `
<h1>Angular Error Handling</h1>
<button (click)="fetchData()">Fetch Data</button>
@if (data$ | async; as data) {
@if (data.error) {
<p>Error: {{ data.error }}</p>
} else {
<p>Data: {{ data | json }}</p>
}
}
`,
})
export class AppComponent {
private readonly http = inject(HttpClient);
error: string | null = null;
data$: Observable<Data | {error: string}>
fetchData() {
this.data$ = this.http
.get('http://some-wrong-api.com/wrong-endpoint')
.pipe(
catchError((error) => {
// notice the usage of the `of` function,
// as `catchError` callback *must* return another Observable
return of({error});
})
);
}
}
As we can see, this is even simpler, and the process is way more streamlined: we do not subscribe to anything, we know we either have the SomeData
object or an error object and use that fact in the template to display the appropriate UI.
If your application is RxJS-heavy, which manby Angular apps are, you might be interested in streamlining the error handling process. This can be achieved, among other things, via utilizing custom RxJS operators. To learn more about handling error (and loading) states with custom RxJS code, you can check out this article by Eduard Krivanek, specifically the section 7.
Now, we have explored how to handle HTTP errors with RxJS, but this is the most localized way of handling it; however, lots of cases of HTTP failures might require a more generic way of dealing with them, such as showing a toast message, or, in the case of some really low-priority requests, just logging some diagnostics and not showing anything to the user. Let's see how this is accomplished.
Handling HTTP errors with interceptors
If you thought "well, we can use interceptors to handle such scenarios", you've guessed it right! If you're unfamiliar with interceptors, check out one of my previous articles covering them in depth.
So, with interceptors, we can easily catch errors and apply some generic handling logic:
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const toastService = inject(ToastService);
const diagnostics = inject(DiagnosticsService);
return next(req).pipe(
catchError((res) => {
if (res.type === HttpEventType.Response && res instanceof HttpErrorResponse) {
toastService.showError('An error occurred while fetching data!');
diagnostics.logError(res);
return of(res);
}
})
);
};
As we can see, we can use the catchError
operator to catch the error and perform some operations, such as showing a toast message or logging the error to some diagnostics service. In the end, we just return the same response to also allow for local error handling (changing something in the UI) as we showed int he previous example.
But this approach can be taken even further! What if we want to log the diagnostics always, but only show the toast message in around 80% of the cases? Well, we will need a way to understand which requests need to show the toast message and which ones don't. Our first instinct might be to add a custom header or some query parameter to the request, but Angular actually provides a built-in way to communicate with interceptors from your services known as HttpContext
.
This is a context object that can be passed to the request and can be used to store any data we want. This object is also available in the interceptor. So, let's see how we can use it:
// creating the token with data to pass with the request
export const NoToastMessage = new HttpContextToken<boolean>(() => true);
@Injectable()
export class SomeService {
private readonly http = inject(HttpClient);
getData() {
return this.http.get('some.url', {context: NoToastMessage});
}
}
Now, we can slightly modify our interceptor to account for this scenario:
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const toastService = inject(ToastService);
const diagnostics = inject(DiagnosticsService);
return next(req).pipe(
catchError((res) => {
if (res.type === HttpEventType.Response && res instanceof HttpErrorResponse) {
// log the diagnostics anyway
diagnostics.logError(res);
if (!req.context.get(NoToastMessage)) {
// if there is not token, show the toast
toastService.showError('An error occurred while fetching data!');
}
return of(res);
}
})
);
};
This way, our interceptor will have more granular control over how to handle a particular request, and not show the toast message unless we want to.
Finally, there is one issue that can be covered when talking about interceptors in the context of error handling, and that is custom API error responses. See, sometimes certain APIs, instead of responding with familiar error codes like 404, 500, etc, respond with a custom error object. Something like 200 OK, but the body of the response is {success: false, error: 'Some error'}
.
Now, this is not a problem in and of itself, and the validity of this approach is out of the scope of this article, however, it creates a lot of boilerplate, since we essentially have to handle the error twice: check for a network error (which can still happen regardless of the server response), and then write an if
statement which checks if the actual response is successful.
However, with interceptors, we can avoid this, and do this checking once and raise an error so that it can be handled further down the line:
export const nestedErrorInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
map(res => {
if (res.type === HttpEventType.Response) {
const body = res.body as {success: boolean, message?: string};
if (body.success === false) {
throw new HttpErrorResponse({error: body.message});
}
return res;
}
return res;
}),
);
}
This helps us remove a lot of friction when it comes to HTTP responses and different sorts of APIs we might encounter when building a frontend application.
Now, as we touched all of these topics that were quite "old", we are now going to circle back to synchronous error handling, as we are about to cover the shiniest new feature of Angular 16+ - signals.
Errors with Signals
First, before we move forward, we must understand what we mean by "error with Signals". After all, signals are wrappers around values, and they are synchronous, so speaking about "errors within signals" makes as much sense as speaking about "errors with variables".
And it is true that conventional signals (created with the signal
function) are not capable of throwing errors. However, the situation gets more complicated when we start using signals that are computed
. See, computed signals rely on callback functions that track other signals and execute to calculate the new value of the computed signal. This means that if an error occurs in the callback function, it will be thrown and can be caught. For instance, consider this piece of code:
@Component({/* */})
export class SomeComponent {
count = signal(0);
doubleCount = computed(() => {
if (this.count() < 0) {
throw new Error('Count cannot be negative!');
}
return this.count() * 2;
});
}
Now, if we set the value of count
to a negative number, the computed signal will throw an error. But what does that mean? When will the error "happen"?
To understand this, we need to go back to the basics and understand how computed signals work. Computed signals in their nature are lazy, which means Angular tries to prevent executing the callback function (which might be costly) as much as possible. For instance, the very first time the callback executes is not when the computed signal is created, but when it is first read (meaning when we "call" the computed signal, like this.doubleCount()
)
Now, when that callback executes for the first time, Angular takes note of what other signals are being used within it (in our case, the count
signal), and make a list of them to "track" in order to be able to update the value of the computed signal when one of those tracked signals changes its value.
However, here the signals also work in a lazy way, meaning when the value of a tracked signal changes, the callbacks does not re-execute immediately; instead, the computed signal is marked as "dirty", and the code moves on. But when this "dirty" computed signal is read again, Angular now knows its value might have changed, and re-executes the computation callback to get the new value.
This is when the error happens. So, to put it in a few words, we can be sure that signal errors do not happen randomly at some point where we update some (other) signal, but when a computed signal is being read.
This gives us some tools to help handle these errors.
- If the computed signal throws an error because of some issue or edge case (like using some weird API, etc), it is usually easier to fix the issue rather than bother with error handling.
- If we own the computed signal (meaning it's not coming from a library or some other code that we cannot change), we can read it in a
try
/catch
block and handle the error gracefully. This is not very enjoyable, but is the only way to handle such an error - Finally, if we do own the code of the computed signal, and we anticipate some errors, it is best to just handle them within the callback function and return a default value.
@Component({/* */})
export class SomeComponent {
// we are using some library we cannot change
private readonly utilities = inject(UtilitiesService);
count = signal(0);
newCount = computed(() => {
try {
// we might expect an error coming from the library
const newValue = this.utilities.calculateNewValue(this.count());
return newValue;
} catch {
// if the library throws an error, we can return some default
return 0; // or some other default value
}
});
}
Now, this is way better than putting try
/catch
blocks all over the place. Note that this approach is also better because, more often than not, computed signals are created to be consumed in templates, and the Angular template syntax does not have an equivalent of the JavaScript try
/catch
statement. So, it would be a complex task to figure out how to handle an error in a computed signal that is being used in the UI.
It is important to state that this approach is useful when we simply want to provide a fallback value if there's an error. If we want to perform a side-effect (like displaying a toast message as covered previously), we should fully forego the computed
signal and instead use an effect
to cover possible scenarios and courses of action.
@Component({/* */})
export class SomeComponent {
private readonly utilities = inject(UtilitiesService);
count = signal(0);
newCount = signal(0);
constructor() {
effect(() => {
try {
const newValue = this.utilities.calculateNewValue(this.newCount());
} catch {
// show a toast message
}
});
}
}
However, we can go even further. If we want to return the previous value if there is an error, instead of using the computed
function, we can opt for the new linkedSignal
utility. With linkedSignal
, we have access to the previous state, we can use it to keep the value and "swallow" the error:
@Component({/* */})
export class SomeComponent {
private readonly utilities = inject(UtilitiesService);
value = signal(0);
computedValue = linkedSignal<number, number>({
source: this.value,
computation: (previous, current) => {
try {
const newValue = this.utilities.calculateNewValue(current.source);
} catch {
return previous;
}
}
}).asReadonly();
}
Here, we take another signal and run a computation on it. If it succeeds, we have a new value, and if there's an error, we simply pick the previous value that linkedSignal
provides for us, and return it, so the consumer is not even aware of the error. We also use the asReadonly
method to make sure the signal is not writable, as if it were a simple computed property.
Note: This approach highly depends on what we are trying achieve, and more often than not it is wiser to not chew up the error and instead handle it in different ways.
Now, the next way of getting an error in a signal pipeline is when creating a signal based on an Observable using the toSignal
functions. In this case, however, we do not have an option of using the try
/catch
approach, and instead have to go back to the previous section of this article and handle the error within the RxJS pipeline using the catchError
operator. We can use it in the same fashion as in the previous example, returning an "error object" that could be used in the future when reading the signal both in the template and component code.
We can also blend it with the previous example with linkedSignal
to keep the previous value of the observable-based signal in case of an error:
@Component({/* */})
export class SomeComponent {
private readonly http = inject(HttpClient);
rawData = toSignal(this.http.get('http://some-wrong-api.com/wrong-endpoint').pipe(
catchError((error) => {
// return an error object
return of({error});
})
));
data = linkedSignal<{error: string} | SomeData, SomeData>({
source: this.rawData,
computation: (previous, current) => {
if (current.error) {
return previous; // return the previous value
}
return current; // return the new value
}
}).asReadonly();
}
Finally, we are arriving at the end of this article, and are about to cover HTTP calls again; this is because of the new tools introduced by Angular that allow us to perform HTTP calls in a reactive fashion and using signals, namely, the Resource API.
Errors with Resources
First, we should make clear that since all the types of resources (resource
, rxResource
and httpResource
) have the same API signatures, especially when it comes to errors, we will simply cover the httpResource
scenario and the rest will be similar.
If you're unfamiliar with the Resource API, you can check my previous article where we dive deep into this topic.
Now, let's see how we can handle an HTTP error with the httpResource
API. Basically, we have two scenarios: either we display some fallback UI, or we perform some sort of side-effect (again, like showing a toast message). The first scenario is as easy as it gets:
@Component({
selector: 'app-root',
template: `
<button (click)="data.reload()">Reload Data</button>
@if (data.hasValue()) {
<!-- normal UI -->
} @else if (data.hasError()) {
<!-- fallback UI in case of error -->
}
`,
})
export class SomeComponent {
data = httpResource(() => 'http://some-wrong-api.com/wrong-endpoint');
}
Here, we make use of the hasError
method to determine if an error happened previously, and display the UI accordingly. But what about side-effects? Well, the hasError
method can be tracked by effect
, so we can write a very simple effect to determine if something needs to be done:
@Component({...})
export class SomeComponent {
data = httpResource(() => 'http://some-wrong-api.com/wrong-endpoint');
constructor() {
effect(() => {
if (this.data.hasError()) {
// show a toast message or something else
}
});
}
}
So, with the Resource API, we now can easily handle errors in a very simple and straightforward way, both in the template and in the TypeScript code, as opposed to computed signals and signals created from other Observables.
Conclusion
As we saw, the topic of error handling in Angular is robust and full of different precarious scenarios. However, we also see that Angular equips us with the best tools to help handle such scenarios, and makes us remember that what separates a poorly-designed application from a great product is the ability to adapt to different user scenarios.
Small Promotion
My book, Modern Angular, is now in print! I spent a lot of time writing about every single new Angular feature from v12-v18, including enhanced dependency injection, RxJS interop, Signals, SSR, Zoneless, and way more.
If you work with a legacy project, I believe my book will be useful to you in catching up with everything new and exciting that our favorite framework has to offer. Check it out here: https://www.manning.com/books/modern-angular
P.S If you want to learn about error handling and other scenarios with Signals, check out the 6th and 7th chapters of my book ;)
