Search code examples
c#asp.net-mvc-4exceptioninternationalizationerror-logging

How to log exception in InvariantCulture?


In my MVC solution I have two handlers for exception. First logs exceptions to Logs database:

public sealed class LogErrorAttribute : Attribute, IExceptionFilter
{
    public void OnException(ExceptionContext filterContext)
    {
        // some handling with filterContext.Exception
    }
}

Second shows exception to user:

public sealed class AsyncAwareHandleErrorAttribute : HandleErrorAttribute
{
    public override void OnException(ExceptionContext filterContext)
    {
         // some handling with filterContext.Exception    
    }
}

Both of them are fired when any unhandled exception occurs:

throw new ArgumentNullException("email", i18n.someErrorMessage);

i18n.someErrorMessage is translated string from resx file.

In both handlers in filterContext.Exception.Message I have got string which is already translated.

How can I log exception message only in CultureInfo.InvariantCulture which is set to en-US in my solution?


Solution

  • The notion that exceptions should contain their own human-readable messages is very prevalent but also very misguided.

    In concept,

    The message conveyed by an exception is the type of the exception along with any member variables contained within the exception.

    What this also means is that:

    You have to have a different exception class for every single conceptually different exception that you throw.

    You cannot have a wildcard "MyAllPurposeException" and differentiate among conceptually different exception situations by constructing different human-readable messages at the point where you throw them. The entity that will be trying to make sense out of your exceptions should not be limited to humans: conceptually, a catch block also has the right to be able to make sense out of exceptions thrown by your code, at the very least in the context of unit tests, but often also in production code.

    For convenience we often just stuff a human readable message into our exceptions, but that's just a quick and dirty solution. When you have an application with actual real-world internationalization needs you can't be doing hacks like that.

    So, my recommendation would be to never add any message to an exception. Let the Message member be blank; I would go as far as to say that the inclusion of the string Message member in the original Exception class was a mistake on behalf of the designers of the runtime of the language. Instead, the handler of an exception, which knows what to do with it, should decide whether a human-readable message should be generated for the exception, and if so, whether it should be readable only by programmers or also by users, and thus, what the locale (culture) should be.

    Conceptually, you would have a two-dimensional table where on the X axis you would have locales, on the Y axis you would have exception type names, and each cell in the table would contain a string such as "Parameter %d cannot be null", in the right language. The first column would presumably be the neutral locale, i.e. the Invariant culture, containing programmer-readable messages.

    Of course, in reality it will need to be slightly more complicated than that, because you will have to somehow read the type-specific members of the exception and pass them to the string formatting function. For exceptions that you have control over, you can add an overridable and implement it like this:

    public ParameterCannotBeNullException extends MyException
    {
        private final int parameterNumber;
    
        public ParameterCannotBeNullException( int parameterNumber )
        {
            this.parameterNumber = parameterNumber;
        }
    
        public override String FormatMessage( String localeSpecificMessage )
        {
            return String.Format( localeSpecificMessage, parameterNumber );
        }
    }
    

    For exceptions that you have no control over, you can have a huge cascaded if statement, going like this:

    String formatString = (look it up by exception.GetType() and locale)
    String message;
    if( exception is SomeException )
    {
         SomeException temp = (SomeException)exception;
         message = String.Format( formatString, temp.X );
    }
    else if( exception is SomeOtherException )
    {
         SomeOtherException temp = (SomeOtherException)exception;
         message = String.Format( formatString, temp.A, temp.B );
    }
    else
    {
         message = exception.GetType().Name;
    }
    

    One final thing: sometimes while writing code we realize that we have come across a situation where an error may occur, so there is a need for a new exception to be thrown, but we do not want to interrupt what we are doing and go declare a new exception class at that moment. For such situations I have found it useful to have a GeneralPurposeException which does in fact accept a string in its constructor containing a programmer-readable message. But this class contains a great big huge comment that reads like this:

    XXX FIXME TODO:

    For development purposes only!

    Thus class must not make it to production!