Click here to Skip to main content
13,705,212 members
Click here to Skip to main content
Add your own
alternative version

Stats

40.4K views
30 bookmarked
Posted 29 Dec 2014
Licenced CPOL

MVC & API Exception Handling using Filters and Angular JS

, 29 Dec 2014
Rate this:
Please Sign up or sign in to vote.
This article will help you to handle all types of errors in MVC platform; MVC Views, API, 404.0, Synchronous/Asynchronous Calls all with same user experience centrally.

Introduction

In this article, I will cover the whole concept of exception handling in an (MVC & API) application supported by Angular JS. It tries to handle all types of exceptions (self raised business, unhandled and 404) centrally!, in a way that informs user about the error and also logs enough information for system administrators for bug fixing and system maintenance.

Background

We all have suffered from injecting Try/Catch blocks in our applications and we have experienced the pain of maintaining, managing and logging the information in various classes. However, exception handling has to be centrally managed and the code should be actually free of all the unnecessary Try/Catch blocks in all layers. Programming based on exception handling blocks is like spaghetti code style programming with GoTo! As I believe, a proper design and message flow between different layers should not be based on exception handling blocks and we really should avoid them.

So what types of error are we going to cover?

Circumstance User Experience
1. A route does not match any defined route at all. PageNotFound, URL will be logged.
2. A route matches the URL, with incorrect Controller PageNotFound, URL will be logged.
3. A route matches the URL, with incorrect Action PageNotFound, URL will be logged.
4. A Business Exception occurs A meaningful message needs to be displayed, no logging required.
5. An Unhandled Exception occurs A generic message needs to be displayed, actual exception message needs to be logged.

End Result

And the end result would look like:

1. Page Not Found

Referring to the above table, for the first, second and third case, we will have this screen:

2. Generic Message

If we had an unhandled exception or an exception which the message shouldn't have been shown to the user:

3. Specific Message

If we had a business exception that its message was needed to be sent to the UI:

And this is how we should be coding really:

public ActionResult GetAllUniversities()
{
    throw new UnixBusinessException("The user has exceeded the quota limit, please buy storage first");
}

A. Backend

I categorized these cases into the above three user user experiences, however there are many circumstances in which we will have either of those messages. As I mentioned, we are not going to cover them case by case as they happen, but we will have a centralized exception handling and logging mechanism, so all our system use cases would be Try/Catch and Logging Free!

Alright, that was the design, let's do some engineering stuff, and build something.

One place to catch all exceptions is Application_Error in Global.asax (oooh, seems quite high level), we will leave this place clean and we don't touch it, as it's for all high level and critical operations!. So no Global.asax mess at all.

protected void Application_Error()
{
    // Nothing here at all.
}

Where else can we catch exceptions in MVC, yes, in individual controllers, behind actions or on a controller level, but it is not clean as you have to do for each controller, or you might do it for a parent controller !? But what would you do for the rest of the application? Nah, we aren't gonna do that either. We need something cleaner. So no Controller mess as well !

protected override void OnException(ExceptionContext filterContext)
{
    base.OnException(filterContext);
}

Now what else? Yes MVC Filters. It is one place for the whole application, we do have access to the HttpContext, and we can turn it on/off for each controller or for the whole application quite easily. They have been created for this purpose.

So I will create an Error Filter for MVC views and I will inject a logger into its constructor. Behind this logger, I have put Elmah to manage my exceptions.

public class CustomErrorFilter : HandleErrorAttribute
{
    private readonly IUnixLogger logger;

    public CustomErrorFilter()
        : this(DependencyResolver.Current.GetService<IUnixLogger>())
    {
    }

    public CustomErrorFilter(IUnixLogger logger)
    {
        this.logger = logger;
    }

    public override void OnException(ExceptionContext filterContext)
    {
        //No further processing when the exception is handled or custom errors are not enabled.
        if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)
        {
            return;
        }

        if (!(filterContext.Exception is UnixBusinessException))
        {
            //Logging the actual exception
            logger.LogException(filterContext.Exception);

            //Changing exception type to a generic exception
            filterContext.Exception = new Exception
        ("An error occurred and logged during processing of this application.");
        }

        base.OnException(filterContext);
    }
}

There are three very important points here, first if the exception is handled or the custom error messages are disabled, we won't go through any nice and clean exception handling any more. And the second is that if the exception is a deliberately thrown business exception, there is no need to log that. And the third is that when we logged an unhandled exception, we will replace the exception with a generic exception so critical error messages wouldn't be exposed to outside world.

Note *) I am using Elmah for this application which is hidden behind my IUnixLogger. I wouldn't go through its configuration as it is quite easy and straightforward and you can get it from its web site.

At the end, I would like to show the user a message via the Error.cshtml in the Shared folder.

@model HandleErrorInfo
@{
    Layout = null;
}
@Model.Exception.Message

Note *) I am using the concept of Area in MVC, however I have put this shared view outside Area folder in the usual Views folder, so all views across the application would benefit from that.

Note *) To practice DRY, we have only one view for these error pages, that doesn't have any Layout. The reason is if it had any layout, at the time I want to render it inline, it would include all the script and HTML stuff along with it, which is not what we want to do.

So what else, yes, registering the filter to MVC filter collection, and turning custom errors on:

filters.Add(new CustomErrorFilter());

<customErrors mode="On" defaultRedirect="~/Error">
</customErrors>

As you can see from the code, I am just throwing a business exception during a call to render one action and the result is as below without any other interference:

public ActionResult GetAllUniversities()
{
    throw new UnixBusinessException("The user has exceeded the quota limit, please buy storage first");
}

And if the exception was an unhandled exception that the system had thrown, like:

public IEnumerable<UniversityViewModel> GetAllUniversities()
{
    throw new Exception("Sensitive Info");
}

The result would be:

Alright, what we have achieved so far is a centralized exception handling mechanism for Business and Unhandled exceptions for simple MVC views. Now what happens if we have Ajax calls to Web API method?

The answer needs more work as the Error Attributes for API are a bit different as they inherit from ExceptionFilterAttribute:

public class CustomApiErrorFilter : ExceptionFilterAttribute
    {
        private readonly IUnixLogger logger;

        public CustomApiErrorFilter()
            : this(DependencyResolver.Current.GetService<IUnixLogger>())
        {
        }

        public CustomApiErrorFilter(IUnixLogger logger)
        {
            this.logger = logger;
        }

        public override void OnException(HttpActionExecutedContext filterContext)
        {
            //Exact message for business exceptions which have been thrown deliberately need to be shown
            //However for other types of unhandled exceptions a generic message should be shown.
            var exceptionMessage = string.Empty;
            if (filterContext.Exception is UnixBusinessException)
            {
                exceptionMessage = filterContext.Exception.Message;
            }
            else
            {
                //Logging Exception
                logger.LogException(filterContext.Exception);

                //Changing exception message to something generic.
                exceptionMessage = "An error occurred and logged during processing of this application.";
            }

            //Throwing a proper message for the client side
            var message = new HttpResponseMessage(HttpStatusCode.InternalServerError)
            {
                Content = new StringContent(exceptionMessage),
                ReasonPhrase = exceptionMessage
            };

            throw new HttpResponseException(message);
        }
    }

Don't panic as it is the same code except the last couple of lines. The first segment in OnException is to distinguish between self thrown business exceptions and unhandled exceptions. The second bit however is basically creating a proper HttpResponseException and sending it through to the browser.

Don't forget to add this attribute to Api attribute collection which is of type HttpFilterCollection:

filters.Add(new CustomApiErrorFilter());

and that's it. API is now supported. Yaaay! The inetersting part? The user would have exactly the same experience for usual MVC View rendering and also API calls. All managed from the same spot. Error Filters.

B. Front End (Angular JS)

Now the front end stuff. For the front end as well, we need to have a centralized location to capture, handle and show the exceptions. What is the best place to capture all request information, responses, and there errors? Services, Factories, Controllers, Views? None!!! as we need a clean and centralized code to practice DRY.

So what's the answer? Yessss, Interceptors. A place that we have access to information before it is handed to the View and before it reaches the MVC server.

//Registering Interceptors
ngModule.config(['$httpProvider', function ($httpProvider) {

    $httpProvider.interceptors.push(function ($q, $rootScope) {
        
        return {
            request: function (config) {

                //the same config / modified config / a new config needs to be returned.
                return config;
            },
            requestError: function (rejection) {

                //Initializing error list
                if ($rootScope.errorList == undefined) {
                    $rootScope.errorList = [];
                }

                $rootScope.errorList.push(rejection.data);

                //It has to return the rejection, simple reject call doesn't work
                return $q.reject(rejection);
            },
            response: function (response) {

                //the same response/modified/or a new one need to be returned.
                return response;
            },
            responseError: function (rejection) {

                //Initializing the error list
                if ($rootScope.errorList == undefined) {
                    $rootScope.errorList = [];
                }

                //Adding to error list
                $rootScope.errorList.push(rejection.data);

                //It has to return the rejection, simple reject call doesn't work
                return $q.reject(rejection);
            }
        };
    });
}]);

There are two functions which are important here, requestError, and responseError. So if you look at the code, there is no difficulty here, except the last line in these two functions:

return $q.reject(rejection);

This basically means I am propagating the exception up to the call stack. Let's start with a question in mind: Have you ever had this problem that always your success block in Ajax calls are just called, although you are raising an exception in your backend? This is the reason:

If like the other methods I simply return the rejection, therefore the following Asynchronous calls would not be notified that an error has been occurred. So for example if I don't call $q.reject no error function in $http calls would be triggered:

var deferred = q.defer();

http({
    url: url,
    method: 'GET',
    params: argument,
    headers: {
        'X-Requested-With': 'XMLHttpRequest'
    }
}).success(function (data) {
    deferred.resolve(data);
}).error(function (data, status) {
    deferred.reject(data);
});

return deferred.promise;

and therefore we can't notify the user that an error has occurred. So it's quite important to reject and also to return this rejection.

Referring to the interceptor again, I am just collecting all the errors in the errorList angular model object and then I can list them in my Layout page (or SPA home screen) like below:

<div class="alert alert-danger" 
role="alert" data-ng-repeat="error in errorList">
    <p>{{ error }}</p>
</div>

I think it's a fantastic use of Angular stuff. What do you think?

Now the Last Piece: 404.0 Not Found Error

This is one of the tricky parts. MVC doesn't have a nice and elegant approach to handle 404. So we need to get our hands dirty. I am not going to use custom IIS error pages, as they don't give me enough flexibility and control on my 404 error handling process. Therefore I would be creating my own Controller Factory, so if I had an incorrect route or a correct route with incorrect controller / action values, I would send the PageNotFound view to the browser instead of the ugly error message.

public class UnixControllerFactory : DefaultControllerFactory
    {
        /// <summary>
        /// Instantiates the proper controller, including a right 404 error page
        /// </summary>
        /// <param name="requestContext"></param>
        /// <param name="controllerType"></param>
        /// <returns></returns>
        protected override IController GetControllerInstance
        (RequestContext requestContext, Type controllerType)
        {
            try
            {
                //Try to instantiate the controller as usual
                var controller = base.GetControllerInstance(requestContext, controllerType);
                return controller;
            }
            //If failed, instantiate 404 and return
            catch (HttpException ex)
            {
                if (ex.GetHttpCode() == 404)
                {
                    MyController errorController = new ErrorController();
                    errorController.SendHttpNotFound(requestContext.HttpContext);
                    return errorController;
                }
                else
                {
                    throw ex;
                }
            }
        }
    }

In the above code snippet, what I am actually doing is trying to create a controller as usual, Failed? Then create 404 view and send it to the browser.

I have created SendHttpNotFound in my base controller which simply creates a 404 View and sends it through. However, I have allocated a totally separate View for PageNotFound errors which is different from the previous error page.

Note *) Don't forget to log the incorrect URL here or in that specific action of Error Controller.

Note *) Another interesting point about this approach is that it's not redirected from another page, like the approach with IIS custom error pages. It is a view which has been meant to be 404 handler originally.

Points of Interest

I believe the interesting point about this approach is that it covers all possible exceptions centrally, for MVC and Web API, in a way that the user has the same user experience for both. Also the way I have managed 404 gives the developer enough flexibility to handle 404 errors completely with full flexibility. For example, you can also manage an existing view with incorrect Object Id as a 404 error without any extra effort, just call SendHttpNotFound in any controller action.

Another aspect is that you can decide what exceptions to log simply by inheriting that exception from your main Business exception class, the rest would be automatic.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Mahdi K.
Technical Lead
Australia Australia
No Biography provided

You may also be interested in...

Pro
Pro

Comments and Discussions

 
Questionwhat if the action name is different from the view name ? Pin
okansarica15-Feb-16 6:11
memberokansarica15-Feb-16 6:11 
QuestionHello , its Pin
jitendra prajapat21-Jan-16 22:13
memberjitendra prajapat21-Jan-16 22:13 
GeneralMy vote of 1 Pin
12-Jan-15 22:08
member12-Jan-15 22:08 
GeneralRe: My vote of 1 Pin
Mahdi K.12-Jan-15 22:14
memberMahdi K.12-Jan-15 22:14 
GeneralRe: My vote of 1 Pin
12-Jan-15 23:26
member12-Jan-15 23:26 
GeneralRe: My vote of 1 Pin
Mahdi K.12-Jan-15 23:29
memberMahdi K.12-Jan-15 23:29 
GeneralRe: My vote of 1 Pin
8-Jun-15 21:35
member8-Jun-15 21:35 
GeneralMy vote of 2 Pin
Anibal_Ven31-Dec-14 7:31
memberAnibal_Ven31-Dec-14 7:31 
GeneralRe: My vote of 2 Pin
Mahdi K.31-Dec-14 14:52
memberMahdi K.31-Dec-14 14:52 
GeneralRe: My vote of 2 Pin
Aurimas1-Jan-15 23:49
memberAurimas1-Jan-15 23:49 
GeneralGreat article, but I want to add something Pin
Aurimas30-Dec-14 0:17
memberAurimas30-Dec-14 0:17 
GeneralRe: Great article, but I want to add something Pin
Sacha Barber30-Dec-14 0:56
mvpSacha Barber30-Dec-14 0:56 
GeneralRe: Great article, but I want to add something Pin
Mahdi K.31-Dec-14 15:13
memberMahdi K.31-Dec-14 15:13 
GeneralRe: Great article, but I want to add something Pin
Mahdi K.31-Dec-14 15:12
memberMahdi K.31-Dec-14 15:12 
GeneralRe: Great article, but I want to add something Pin
Aurimas1-Jan-15 23:59
memberAurimas1-Jan-15 23:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01-2016 | 2.8.180920.1 | Last Updated 30 Dec 2014
Article Copyright 2014 by Mahdi K.
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid