Created
September 19, 2022 11:59
Using Type Guards To Narrow Down Error Handling Types In Angular 14
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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