Error handling and reporting

Built in patterns for error handling and reporting.

Mobiz Error Handling and Tracking Handbook

Mission statement

Error handling often becomes a confusing part of system architecture, especially as the codebase grows the exact handling of errors can easily end up scattered around the code-base with different strategies in place depending on the developer implement each feature. Therefore strong guiding patterns have to be in place: Where should the error be handled and how? Where to throw exceptions and where to catch.

It is important to maintain strong patterns for the flow of errors in a system and preferably maintain a set of simple and easily understandable principles which all developers can follow in the codebase without have to memorise complex infrastructure patterns.

Mobiz Platform provides such pattern as well as the required infrastructure to maintain the patterns. We are not inventing the wheel here. The principles are simply a set of best practices and opinionated infrastructure which hopefully serves the goal of simplifying and maintaining clear development patterns for handling errors and helps to raise the overall quality bar for the delivered product as well it’s codebase.

Principal assumptions

We define 4 main kind / class of errors

To maintain clear classification in relation to error handling and error flow of control, especially in terms of providing a consistent and user friendly UX experience for end-users when errors occur, we define 4 kinds of errors:

  1. Forbidden: Attempt was made to access a feature or data which the calling user did not have sufficient permission to authorize the call.
  2. Validation: End-user provided invalid data as input argument.
  3. InvalidArgument: Invalid internal data or configuration was provided as part of argument or configuration of a method or a feature which resulted in an error condition. It is important to notice the difference between validation of customer input vs. unexpected data or configuration inside the running code which is this case here.
  4. InternalServerError: Unexpected error occurred. This is the most critical type of error.

Errors are handled and managed in code “entry points”

Of course error raising and handling should never be considered a normal part of software logical flow. Raising an exception is expensive in environments like .NET and should be done with great care.

Our basic assumption is that errors should be caught, managed and logged at the entry point of the software application or service.

What is an entry point?

  • In relation to ASP.NET we consider each Request an entry point. In this case we provide a “GlobalExceptionFilter” which will handle all exceptions, log the errors and return nicely formatted JSON in align with the “4 kinds of errors pattern” supported by the platform.
  • In other programming artifacts we consider the application / process entry method (main) to be the entry point. This method should therefore contain a Try/Catch clause catching all exceptions with proper handling in accordance to the above defined rules.

Only entry-point error handlers should manage the errors, handle logging etc.

Inside your code you might catch an error as part of retry logic or programming flow.

In this case you might want to log something and try again. But if you encounter an error situation where you cannot recover and resume the error should “bubble up” to the entry point.

This is where there error should always be logged and managed (concluded). If the developer has extra information which might provide helpful for diagnostic of the problem such information can always be added to the Data property of the Exception instance before re-throw.

In this case the logging infrastructure will ensure this data is forwarded and reported as part of error management and reporting infrastructure.

The central error logging mechanism is responsible for generating a Tracking Identifier which is logged with the error and also returned to the client for future reference. This is represented via two fields returned to client as part of EVERY error response.

  • TrackingId: A token consisting of string and/or numbers which can be used to search and find all logged error information in all logging targets in relation to the particular error.
  • ErrorReference: A valid URL which should be able to display some kind of error report for the given TrackingId. This might by a simple matter of the backend providing direct URL to logging system like Sentry or Exceptionless or even a custom built view for customers to keep track of error handling and management.

Key design concepts

We specify 4 kind of errors each with a defined problem scope and rules for how to report to user:

  • Forbidden: Security Authorization was denied
    • Raised in code by throwing an exception of type Mobiz.Core.Security.ForbiddenException
    • The error message returned from server to show to the user is always a constant “Operation Forbidden.”
    • Error response also contains details for optional alanysis of the problem, including tracking ID to enable finding the logged instance of the error in backend logging infrastructure.
  • Validation: Client provided invalid data as form input. Client is notified and the user can retry with updated arguments.
    • Raised in code by throwing an exception of type Mobiz.Core.ErrorHandling.ValidationException
    • The error message returned from server to show to the user is localised validation message from the backend, localize by User’s active locale selection.
    • Error response also contains details for optional alanysis of the problem, including tracking ID to enable finding the logged instance of the error in backend logging infrastructure.
  • Invalid Argument: Internal problem in the services caused by invalid configurations are unexpected data was encountered. Can not be recovered from by simple client retry.
    • Raised in code by throwing an exception of System.ArgumentException or derived exception type.
    • The error message returned from server to show to the user is always a constant “Invalid argument.”
    • Error response also contains details for optional alanysis of the problem, including tracking ID to enable finding the logged instance of the error in backend logging infrastructure.
    • NOTE: To override the “hard-coded” user message the developer can throw an error of type Mobiz.Core.ErrorHandling.MobizArgumentException in this case developer can provide custom message for the response.Message field which should then be displayed to the user, given that frontend developers are following the defined error handling and reporting patterns.
  • Internal Server Error / Catch-all error":
    • Raised by throwing of any other exception type than the above mentioned ones.
    • The error message returned from server to show to the user is always a constant “Internal server error.”
    • Error response also contains details for optional alanysis of the problem, including tracking ID to enable finding the logged instance of the error in backend logging infrastructure.

Backend returns different error codes and different response details depending on the error kind:

Forbidden Response Error example

Response HTTP Status = 403 / Forbidden

{
   "ErrorType": "Forbidden",
   "Details": => EXCEPTION.Message,
   "ErrorCode": =>EXCEPTION.ExceptionCode,
   "TrackingId": => TRACKING-ID,
   "ErrorReference": => "www.PATH-TO-ERROR-INFORMATION-SYSTEM.com/TRACKING-ID",
   "Message": "Operation Forbidden."
   
}

Validation Response Error example

Response HTTP Status = 403 / Forbidden

{
   "ErrorType": "Forbidden",
   "Details": => EXCEPTION.Message,
   "ErrorCode": =>EXCEPTION.ExceptionCode,
   "TrackingId": => TRACKING-ID,
   "ErrorReference": => "www.PATH-TO-ERROR-INFORMATION-SYSTEM.com/TRACKING-ID",
   "Message": => LOCALIZED BACKEND VALIDATION MESSAGE
   
}

Invalid Argument Error Response example

Response HTTP Status = 400 / BadRequest

{
   "ErrorType": "InvalidArgument",
   "Details": => EXCEPTION.Message,
   "ErrorCode": =>ArgumentException.ParamName,
   "TrackingId": => TRACKING-ID,
   "ErrorReference": => "www.PATH-TO-ERROR-INFORMATION-SYSTEM.com/TRACKING-ID",
   "Message": "Invalid argument."
   
}

Internal Server Error Response example

Response HTTP Status = 500 / InternalServerError

{
   "ErrorType": "InternalServerError",
   "Details": => EXCEPTION.Message,
   "ErrorCode": =>EXCEPTION.ExceptionCode,
   "TrackingId": => LOGSYSTEM-TRACKING-ID,
   "ErrorReference": => "www.PATH-TO-ERROR-INFORMATION-SYSTEM.com/TRACKING-ID",
   "Message": "Internal server error."
   
}

There can only be four types!

We are opinionated in the way we promote handling for each kind of error on client side in regards to user experience:

  • Forbidden User should be notified that permission was denied. No further explaination is required.
    • Recomended UX
      • Dialog Message: “Operation Forbidden.” (As defined by response.Message property)
      • Dialog should provide User to optionally view further Details an information: TrackingId / ErrorReference.
  • Validation User is notified in a friendly dialog (not error dialog) that validation failed and a friendly message from the backend is shown to the user.
    • Recomended UX
      • Message: Display response.Message directly in a user friendly dialog. No details or further information options required.
  • Invalid Argument User is notified with a standard error dialog showing a standard message. “Server failure”. The dialog provides option for “more details” and there user can view the error details, including error message as well as a tracking ID provided by backend to be able to track the exact error condition in error logs where much more details can be found in relation to the error context, stack trace, environment, etc.
    • Recomended UX
      • Dialog Message: ““Invalid argument.” (As defined by response.Message property)
      • Dialog should provide User to optionally view further Details an information: TrackingId / ErrorReference.
  • Internal Server Error * Invalid Argument User is notified with a standard error dialog showing a standard message. “Server error”. The dialog provides option for “more details” and there user can view the error details, including error message as well as a tracking ID provided by backend to be able to track the exact error condition in error logs where much more details can be found in relation to the error context, stack trace, environment, etc.
    • Recomended UX
      • Dialog Message: “Internal server error.” (As defined by response.Message property)
      • Dialog should provide User to optionally view further Details an information: TrackingId / ErrorReference.

Developers should be able to trust that the error will be handled, logged and reported properly

KEEP IN MIND

Only time developer need to catch an exception is in case of logical flow or to add more context information to the exception before re-throw.

Developers coding for the backend can trust that any exception will be caught, managed, logged and reported properly.

C# developers - beware of re-throw!

DON’T DO THIS inside your exception handler to re-throw an exception:

throw ex;

Newer re-throw an exception with the caught instance. This will mess up the stack trace of the original exception.

DO THIS

throw;

This will simply re-throw the original exception, keeping the stack-trace intact.

Ref: https://stackoverflow.com/questions/178456/what-is-the-proper-way-to-re-throw-an-exception-in-c

Server Implementation: GlobalApiExceptionFilter

We maintain the error handling logic for backend web endpoint as Microsoft.AspNetCore.Mvc.Filters.ExceptionFilterAttribute derived type: Mobiz.Core.Web.ErrorHandling.GlobalApiExceptionFilter.

The GlobalApiExceptionFilter has to be registered and this is handled as part of Mobiz Platform WebAPI bootstrapping in Mobiz.Core.Web.Config.WebApiConfig.ConfigureWebApiServices.

The GlobalApiExceptionFilter maintains a registry of error handlers for implementing the above defined strategy of error classification.

  • Each handler implements an interface Mobiz.Core.Web.ErrorHandling.IExceptionHandler.
  • Registered exception handlers:
    • ForbiddenExceptionHandler: Implements the strategy for handling security errors represented by ForbiddenException type.
    • ValidationExceptionHandler: Implements the strategy of validation errors.
    • ArgumentExceptionHandler: Implements the strategy for argument exceptions / Invalid Argument.
    • CatchAllExceptionHandler: Implements the strategy of catch-all / internal server error.
    • DkValidationErrorHandler: Example of how we can extend the error infrastructure. In this case DK API often raised validation errors as SOAP errors. The only way to know if it was validation or pure error was if error message coming from DK contains the string “Validation error:”. In this case, instead of handling it all over the DK code we simply injected a global handler for exactly this error which returns a Validation error and by that established perfectly aligned flow to ensure the validation is displayed to end-user in a nice, user friendly dialog.

The global exception list of handler is currently hard-coded but we will make it extensible for extensions to register extra extra exception handlers.