Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created September 19, 2022 11:59
Using Type Guards To Narrow Down Error Handling Types In Angular 14
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 );
}
}
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 );
}
}
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 );
}
}
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 );
}
)
;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment