254

I have code that is logging Exception.Message. However, I read an article which states that it's better to use Exception.ToString(). With the latter, you retain more crucial information about the error.

Is this true, and is it safe to go ahead and replace all code logging Exception.Message?

I'm also using an XML based layout for log4net. Is it possible that Exception.ToString() may contain invalid XML characters, which may cause issues?

Jesse
  • 8,605
  • 7
  • 47
  • 57
JL.
  • 78,954
  • 126
  • 311
  • 459
  • 1
    You should also look at ELMAH (https://code.google.com/p/elmah/) - A very easy-to-use framework for Error Logging for ASP.NET. – Ashish Gupta Jan 20 '15 at 20:26
  • if you're logging from an asynchronous thread, elmah won't work. You'll throw a null context error. – John Lord May 04 '23 at 14:10

8 Answers8

326

Exception.Message contains only the message (doh) associated with the exception. Example:

Object reference not set to an instance of an object

The Exception.ToString() method will give a much more verbose output, containing the exception type, the message (from before), a stack trace, and all of these things again for nested/inner exceptions. More precisely, the method returns the following:

ToString returns a representation of the current exception that is intended to be understood by humans. Where the exception contains culture-sensitive data, the string representation returned by ToString is required to take into account the current system culture. Although there are no exact requirements for the format of the returned string, it should attempt to reflect the value of the object as perceived by the user.

The default implementation of ToString obtains the name of the class that threw the current exception, the message, the result of calling ToString on the inner exception, and the result of calling Environment.StackTrace. If any of these members is a null reference (Nothing in Visual Basic), its value is not included in the returned string.

If there is no error message or if it is an empty string (""), then no error message is returned. The name of the inner exception and the stack trace are returned only if they are not a null reference (Nothing in Visual Basic).

Community
  • 1
  • 1
Jørn Schou-Rode
  • 37,718
  • 15
  • 88
  • 122
  • 122
    +1 Its very painful to see ONLY that "Object reference not set to an instance of an object" in the logs. You feel really helpless. :-) – Ashish Gupta Jun 04 '11 at 11:51
  • 1
    For the last part, there are Exceptions that don't come with Exception.Message. In function of what you do in the error handling part, you can get to have problems because of the Exception.Message. – Coral Doe Aug 20 '12 at 08:48
  • 71
    It is very painful to see that I wrote code that essentially does the exact same thing that ToString() does. – Preston McCormick Apr 29 '14 at 20:31
  • Exception.ToString() is a write way. Agree. @Ashish Gupta: just in case, you dont need to feel helpless with object refrence. You can always debug through by turning on the CLR exception in case you dont have stack trace to debug certain area. With clr ex turned on, you will directly break on the faulty line. – Kunal Goel Jan 17 '15 at 08:02
  • if you do a custom stack trace, the ILOffset is pretty nice to have in the logs. you can use it to track it down to the line the exception occurred at. – savagepanda Nov 17 '16 at 21:38
  • 1
    @KunalGoel If the log comes from prod and you have no indication what the input was, then no, you can't just "debug through by turning on the CLR exception". – jpmc26 Apr 26 '17 at 21:32
  • 2
    Note, it is the "default implementation of ToString"...(emphasis on "default")..it does not mean that everyone has followed that practice with any custom exceptions. #learnedTheHardWay – granadaCoder Apr 24 '18 at 15:12
69

In addition to what's already been said, don't use ToString() on the exception object for displaying to the user. Just the Message property should suffice, or a higher level custom message.

In terms of logging purposes, definitely use ToString() on the Exception, not just the Message property, as in most scenarios, you will be left scratching your head where specifically this exception occurred, and what the call stack was. The stacktrace would have told you all that.

Wim
  • 11,998
  • 1
  • 34
  • 57
38

Converting the WHOLE Exception To a String

Calling Exception.ToString() gives you more information than just using the Exception.Message property. However, even this still leaves out lots of information, including:

  1. The Data collection property found on all exceptions.
  2. Any other custom properties added to the exception.

There are times when you want to capture this extra information. The code below handles the above scenarios. It also writes out the properties of the exceptions in a nice order. It's using C# 7 but should be very easy for you to convert to older versions if necessary. See also this related answer.

public static class ExceptionExtensions
{
    public static string ToDetailedString(this Exception exception) =>
        ToDetailedString(exception, ExceptionOptions.Default);

    public static string ToDetailedString(this Exception exception, ExceptionOptions options)
    {
        if (exception == null)
        {
            throw new ArgumentNullException(nameof(exception));
        } 

        var stringBuilder = new StringBuilder();

        AppendValue(stringBuilder, "Type", exception.GetType().FullName, options);

        foreach (PropertyInfo property in exception
            .GetType()
            .GetProperties()
            .OrderByDescending(x => string.Equals(x.Name, nameof(exception.Message), StringComparison.Ordinal))
            .ThenByDescending(x => string.Equals(x.Name, nameof(exception.Source), StringComparison.Ordinal))
            .ThenBy(x => string.Equals(x.Name, nameof(exception.InnerException), StringComparison.Ordinal))
            .ThenBy(x => string.Equals(x.Name, nameof(AggregateException.InnerExceptions), StringComparison.Ordinal)))
        {
            var value = property.GetValue(exception, null);
            if (value == null && options.OmitNullProperties)
            {
                if (options.OmitNullProperties)
                {
                    continue;
                }
                else
                {
                    value = string.Empty;
                }
            }

            AppendValue(stringBuilder, property.Name, value, options);
        }

        return stringBuilder.ToString().TrimEnd('\r', '\n');
    }

    private static void AppendCollection(
        StringBuilder stringBuilder,
        string propertyName,
        IEnumerable collection,
        ExceptionOptions options)
        {
            stringBuilder.AppendLine($"{options.Indent}{propertyName} =");

            var innerOptions = new ExceptionOptions(options, options.CurrentIndentLevel + 1);

            var i = 0;
            foreach (var item in collection)
            {
                var innerPropertyName = $"[{i}]";

                if (item is Exception)
                {
                    var innerException = (Exception)item;
                    AppendException(
                        stringBuilder,
                        innerPropertyName,
                        innerException,
                        innerOptions);
                }
                else
                {
                    AppendValue(
                        stringBuilder,
                        innerPropertyName,
                        item,
                        innerOptions);
                }

                ++i;
            }
        }

    private static void AppendException(
        StringBuilder stringBuilder,
        string propertyName,
        Exception exception,
        ExceptionOptions options)
    {
        var innerExceptionString = ToDetailedString(
            exception, 
            new ExceptionOptions(options, options.CurrentIndentLevel + 1));

        stringBuilder.AppendLine($"{options.Indent}{propertyName} =");
        stringBuilder.AppendLine(innerExceptionString);
    }

    private static string IndentString(string value, ExceptionOptions options)
    {
        return value.Replace(Environment.NewLine, Environment.NewLine + options.Indent);
    }

    private static void AppendValue(
        StringBuilder stringBuilder,
        string propertyName,
        object value,
        ExceptionOptions options)
    {
        if (value is DictionaryEntry)
        {
            DictionaryEntry dictionaryEntry = (DictionaryEntry)value;
            stringBuilder.AppendLine($"{options.Indent}{propertyName} = {dictionaryEntry.Key} : {dictionaryEntry.Value}");
        }
        else if (value is Exception)
        {
            var innerException = (Exception)value;
            AppendException(
                stringBuilder,
                propertyName,
                innerException,
                options);
        }
        else if (value is IEnumerable && !(value is string))
        {
            var collection = (IEnumerable)value;
            if (collection.GetEnumerator().MoveNext())
            {
                AppendCollection(
                    stringBuilder,
                    propertyName,
                    collection,
                    options);
            }
        }
        else
        {
            stringBuilder.AppendLine($"{options.Indent}{propertyName} = {value}");
        }
    }
}

public struct ExceptionOptions
{
    public static readonly ExceptionOptions Default = new ExceptionOptions()
    {
        CurrentIndentLevel = 0,
        IndentSpaces = 4,
        OmitNullProperties = true
    };

    internal ExceptionOptions(ExceptionOptions options, int currentIndent)
    {
        this.CurrentIndentLevel = currentIndent;
        this.IndentSpaces = options.IndentSpaces;
        this.OmitNullProperties = options.OmitNullProperties;
    }

    internal string Indent { get { return new string(' ', this.IndentSpaces * this.CurrentIndentLevel); } }

    internal int CurrentIndentLevel { get; set; }

    public int IndentSpaces { get; set; }

    public bool OmitNullProperties { get; set; }
}

Top Tip - Logging Exceptions

Most people will be using this code for logging. Consider using Serilog with my Serilog.Exceptions NuGet package which also logs all properties of an exception but does it faster and without reflection in the majority of cases. Serilog is a very advanced logging framework which is all the rage at the time of writing.

Top Tip - Human Readable Stack Traces

You can use the Ben.Demystifier NuGet package to get human readable stack traces for your exceptions or the serilog-enrichers-demystify NuGet package if you are using Serilog.

Muhammad Rehan Saeed
  • 35,627
  • 39
  • 202
  • 311
11

I'd say @Wim is right. You should use ToString() for logfiles - assuming a technical audience - and Message, if at all, to display to the user. One could argue that even that is not suitable for a user, for every exception type and occurance out there (think of ArgumentExceptions, etc.).

Also, in addition to the StackTrace, ToString() will include information you will not get otherwise. For example the output of fusion, if enabled to include log messages in exception "messages".

Some exception types even include additional information (for example from custom properties) in ToString(), but not in the Message.

Christian.K
  • 47,778
  • 10
  • 99
  • 143
8

Depends on the information you need. For debugging the stack trace & inner exception are useful:

    string message =
        "Exception type " + ex.GetType() + Environment.NewLine +
        "Exception message: " + ex.Message + Environment.NewLine +
        "Stack trace: " + ex.StackTrace + Environment.NewLine;
    if (ex.InnerException != null)
    {
        message += "---BEGIN InnerException--- " + Environment.NewLine +
                   "Exception type " + ex.InnerException.GetType() + Environment.NewLine +
                   "Exception message: " + ex.InnerException.Message + Environment.NewLine +
                   "Stack trace: " + ex.InnerException.StackTrace + Environment.NewLine +
                   "---END Inner Exception";
    }
Carra
  • 17,808
  • 7
  • 62
  • 75
  • 13
    This is more or less what `Exception.ToString()` will give you, right? – Jørn Schou-Rode Feb 01 '10 at 13:04
  • 5
    @Matt: Constructing an instance of `StringBuilder` in this scenario may well be more expensive than two new string allocations, it's highly debatable it would be more efficient here. It's not like we're dealing with iterations. Horses for courses. – Wim Feb 01 '10 at 13:28
  • @Wim: Huh? Forgive may (temporary) witlessness, but there are whole lot more than two instances created. Basically, for each +operator(string,string) you get a new instance. Apart from that, you may be right however, that it will not make any difference. – Christian.K Feb 01 '10 at 13:55
  • 3
    Problem here is, that you'll only get the "InnerException" of the outermost exception. IOW, if InnerException itself has an InnerException set, you'll not dump it (assuming that you want to in the first place). I'd really stick with ToString(). – Christian.K Feb 01 '10 at 13:56
  • @Christian: yes, you're right, for each +, so indeed more than 2. My initial assessment didn't go further than `message=` and `message+=`. It may just balance out in this case to be honest. `String.Format` is probably what I would have used in this specific case (for mainly readability), which I think actually uses `StringBuilder` under the hood. – Wim Feb 01 '10 at 14:13
  • 7
    Just use ex.ToString. It gets you all the details. – John Saunders Feb 01 '10 at 15:13
  • 3
    @Christian: The compiler is sane with multiple +s. See for example "The + operator is easy to use and makes for intuitive code. Even if you use several + operators in one statement, the string content is copied only once." from http://msdn.microsoft.com/en-us/library/ms228504.aspx – David Eison Sep 25 '12 at 18:06
3

In terms of the XML format for log4net, you need not worry about ex.ToString() for the logs. Simply pass the exception object itself and log4net does the rest do give you all of the details in its pre-configured XML format. The only thing I run into on occasion is new line formatting, but that's when I'm reading the files raw. Otherwise parsing the XML works great.

Dillie-O
  • 29,277
  • 14
  • 101
  • 140
2

Ideally, you're best to serialize the entire exception object versus .ToString(). This will encapsulate the entire exception object (all inner exceptions, message, stack trace, data, keys, etc.).

You can then be sure nothing is left out. Plus, you also have the object in a universal format that you can utilize in any application.

    public static void LogError(Exception exception, int userId)
    {
        LogToDB(Newtonsoft.Json.JsonConvert.SerializeObject(exception), userId);
    }
jbrekke
  • 88
  • 6
0

Well, I'd say it depends what you want to see in the logs, doesn't it? If you're happy with what ex.Message provides, use that. Otherwise, use ex.toString() or even log the stack trace.

Thorsten Dittmar
  • 55,956
  • 8
  • 91
  • 139