Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Simon Free
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Simon Free@simonfree )

Using Type Guards To Narrow Down Error Handling Types In Angular 14

By on

Over the weekend, I added an Angular 14 front-end to my Strangler feature flag exploration in Lucee CFML. However, something wasn't sitting right with me: Error handling. In TypeScript, the type of an error variable within a catch block (or Promise callback) is always any. This makes for relatively easy error handling; but, it side-steps the type safety normally provided by the compiler. As such, I wanted to go back and add a Type Guard with a Type Predicate that will help my error handling workflow narrow down the value being caught.

ASIDE: Apparently, as of TypeScript 4, there is now an unknown type which is being used for error handling. I have not personally tried that as I am a few years behind on my TypeScript skills.

In my Angular 14 application, I have an ApiClient class that proxies the HttpClient and is responsible for making requests to the API end-point of my Lucee CFML server. On the Lucee / ColdFusion side of the network, I centralize my error handling and error message translation such that the API will return a message that is safe to show the user. This way, I don't have to recreate all of the error handling logic on both the Server and the Client.

Now, as part of the request workflow in my ApiClient, I intercept error responses and translate them into predicable - and safe - error structures that can be consumed in the rest of the application. Part of this process entails plucking the aforementioned user friendly error message out of the Server response and making it available to the Angular application.

Here's the private method, normalizeError(), that takes the error returned by the HttpClient and makes sure that it has a developer-friendly shape:

export class ApiClient {

	// ... truncated ....

	/**
	* I normalize the given error to have a predictable shape.
	*/
	private normalizeError( errorResponse: any ) : ResponseError {

		// Setup the default structure.
		// --
		// NOTE: The "isApiClientError" property is a flag used in other parts of the
		// application to facilitate type guards, type narrowing, and error consumption.
		var error = {
			isApiClientError: true,
			data: {
				type: "ServerError",
				message: "An unexpected error occurred while processing your request.",
				rootCause: null
			},
			status: {
				code: ( errorResponse.status || 0 ),
				text: ( errorResponse.statusText || "" )
			}
		};

		// If the error data is an Object (which it should be if the server responded
		// with a domain-based error), then it should have "type" and "message"
		// properties within it. That said, just because this isn't a transport error, it
		// doesn't mean that this error is actually being returned by our application.
		if (
			( errorResponse.error?.strangler === true ) &&
			( typeof( errorResponse.error?.type ) === "string" ) &&
			( typeof( errorResponse.error?.message ) === "string" )
			) {

			error.data.type = errorResponse.error.type;
			error.data.message = errorResponse.error.message;

		// If the error data has any other shape, it means that an unexpected error
		// occurred on the server (or somewhere in transit, such as at the CDN, Ingress
		// Proxy, Load Balancer, etc). Let's pass that raw error through as the rootCause,
		// using the default error structure.
		} else {

			error.data.rootCause = errorResponse.error;

		}

		return( error );

	}

}

Ultimately, if the server returns an error response, and the .strangler flag is set to true, it means that the message property embedded within the error response is safe to show the user. Of course, there are many reasons why an HTTP request may fail, having nothing to do with my ColdFusion application's error handling. As such, I have to inspect the HttpClient error for said flag before I attempt to extract the user friendly error message.

Now, even with this predictable shape, the catch blocks and Promise handlers still use any for the error object because there's no guarantee as to where an error came from within the call-stack. But, within the catch blocks, we can apply "type narrowing" techniques in order to add runtime predictability.

"Narrowing" is the process by which TypeScript refines a given value to be of a more specific type. So, in a catch block, we can narrow the error object from the type any down to the type ResponseError, which is being returned by my ApiClient. And to do this, we're going to use a Type Guard.

A Type Guard is a function whose return type is a Type Predicate. A type predicate takes the form of:

parameterName is Type

And, when this Type Guard function returns true, it tells TypeScript that the Type Predicate can be applied and that the value passed to the given guard function is guaranteed to be of the given type. As a result, TypeScript is able to narrow the error type from any down to the type provided by the Type Guard.

In my ApiClient class, I am providing a member method that can be used as a type guard - notice that I am using the isApiClientError flag being provided by the error-normalization method above:

export class ApiClient {

	// .... truncated ....

	/**
	* By default, errors in a catch block are of type "any" because it's unclear where in
	* the callstack the error was thrown. This method provides a runtime check that
	* guarantees that the given error is an API Client error. When this method returns
	* "true", TypeScript will narrow the error variable to be of type ResponseError.
	*/
	public isApiClientError( error: any ) : error is ResponseError {

		return( error?.isApiClientError === true );

	}

}

Now, within my error handling workflow, I can use this isApiClientError() type guard method before attempting to extract the user-friendly error message provided by my ColdFusion server. In this Angular 14 application, I'm implementing this process in the getMessage() method of my ErrorService (which overrides the core implementation of Angular's ErrorHandler):

export class ErrorService implements ErrorHandler {

	private apiClient;

	/**
	* I initialize the API client with the given dependencies.
	*/
	constructor( apiClient: ApiClient ) {

		this.apiClient = apiClient;

	}

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I attempt to extract the human-friendly error message from the given error. However,
	* since there are no guarantees as to where in the application this error was thrown,
	* we will have to do some introspection / type narrowing in order to find the most
	* appropriate error message property to display.
	*/
	public getMessage( error: any ) : string {

		// If this is an API Client error, the embedded message is trusted and can be
		// rendered for the user.
		if ( this.apiClient.isApiClientError( error ) ) {

			return( error.data.message );

		}

		return( "Sorry, we could not process your request." );

	}


	/**
	* I provide a centralized location for logging errors in Angular.
	*/
	public handleError( error: any ) : void {

		// NOTE: In the future, this could ALSO be used to push the errors to a remote log
		// aggregation API end-point or service.
		console.error( error );

	}
	
}

As you can see, in the getMessage() method, I'm using the isApiClientError() method in order to narrow down the error type before stepping into the complex data structure for the human-friendly error message provided by the ColdFusion API.

Now, in my Component-level form processing and error handling, I can catch any error and easily - and safely - hand it off to this getMessage() method:

export class CreateViewComponent {

	// .... truncated ....

	/**
	* I submit the new feature flag for processing.
	*/
	public createFeatureFlag() : void {

		if ( this.isProcessing ) {

			return;

		}

		this.isProcessing = true;
		this.errorMessage = null;

		this.featureFlagService
			.createFeatureFlag(/* ... form data ... */)
			.then(
				( response ) => {

					this.router.navigate([ "/feature-flag", this.form.key ]);

				},
				( error ) => {

					this.isProcessing = false;
					// We're taking the error message, which is currently of
					// type `any`, and we're handing it off to the ErrorService,
					// which will NARROW THE TYPE DOWN to the `ErrorResponse`
					// structure returned by the ApiClient. This allows the
					// ErrorService to safely extract the user-friendly error
					// message returned by the ColdFusion API.
					this.errorMessage = this.errorService.getMessage( error );

				}
			)
		;

	}

}

Fundamentally, my Angular application's runtime behavior is no different than it was before. But now, I have better error handling in place through type guards and type narrowing. This illustrates the primary value of TypeScript (for me): that it forces me to think more deeply about how my code is being run.

Want to use code from this post? Check out the license.

Reader Comments

21 Comments

I think unknown is more appropriate because then compiler forces the consumer to properly typecast the variable before using. I think its good feature to know.

In production application where do you prefer to handle the error and give feedback to user. I noticed some people use a central service to show the error and some folks make it the responsibility of the component making the api call.

15,329 Comments

@Hassam,

Yeah, I only learned about unknown while I was looking up some documentation for the Type Guards. I don't think (or don't remember) hearing about it before that. I agree that it likely makes more sense, at least if you are intending to reach down into the error object.

As far as where to handle the error, I like to do it in the component that made the API calls. In general, I like to keep my components fairly isolated and easy to delete (the one guiding principle that I old highest). As such, let the component read from the Router, do the API call, and manage the errors. This way, if I don't need that component anymore, and I delete, I don't have to find a lot of other files to update - I just delete the component.

So far, this has worked well for me. But, I'm certainly open to being wrong about it.

21 Comments

@Ben,

sounds good, i also prefer to keep the error handling specific to component and have a middleware/interceptor to convert the wording of message.

Post A Comment — I'd Love To Hear From You!

Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.