55

When an exception is thrown (while debugging in the IDE), i have the opportunity to view details of the exception:

enter image description here

But in code if i call exception.ToString() i do not get to see those useful details:

System.Data.SqlClient.SqlException (0x80131904): Could not find stored procedure 'FetchActiveUsers'.
  [...snip stack trace...]

But Visual Studio has some magic where it can copy the exception to the clipboard:

enter image description here

Which gives the useful details:

System.Data.SqlClient.SqlException was unhandled by user code
  Message=Could not find stored procedure 'FetchActiveUsers'.
  Source=.Net SqlClient Data Provider
  ErrorCode=-2146232060
  Class=16
  LineNumber=1
  Number=2812
  Procedure=""
  Server=vader
  State=62
  StackTrace:
       [...snip stack trace...]
  InnerException:

Well i want that!

What would be the contents of:

String ExceptionToString(Exception ex)
{ 
    //todo: Write useful routine
    return ex.ToString();
}

that can accomplish the same magic. Is there a .NET function built in somewhere? Does Exception have a secret method somewhere to convert it to a string?

Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
  • 5
    You'll need reflection; I'm not aware of any built-in code that does this. – SLaks Nov 07 '11 at 16:47
  • You want the IDE debugger window details to be out put to some where? Or just customize the exception object output, where in you just have to append few members of exception object. Still your qs is not soo clear. – Zenwalker Nov 07 '11 at 16:53
  • 1
    Just keep in mind that exceptions can contain exceptions. Sometimes the true error is buried inside another exception. So, whatever code should be in some sort of loop like while (innerException != null) , that way all excpetions will be reported, not just the top level one. – Jon Raynor Nov 07 '11 at 18:10
  • @zenwalker i want an `ExceptionToString` function, that has all the useful power of what Microsoft's own `ExceptionToString` does when it *copies the string to the clipboard*. – Ian Boyd Nov 07 '11 at 18:51
  • 1
    Um... you probably already know this, but `-2146232060` is the same number as the `0x80131904` reported in the exception message. So that information isn't being lost, just represented differently. (And in this case, the hex representation is the more "correct" one for most purposes.) – Daniel Pryden Nov 07 '11 at 19:50
  • @DanielPryden i did indeed. But it's useful to point that out for others googling for the error - to know that one's the signed decimal representation of an `HRESULT`. – Ian Boyd Nov 07 '11 at 20:11

11 Answers11

56

ErrorCode is specific to ExternalException, not Exception and LineNumber and Number are specific to SqlException, not Exception. Therefore, the only way to get these properties from a general extension method on Exception is to use reflection to iterate over all of the public properties.

So you'll have to say something like:

public static string GetExceptionDetails(this Exception exception) {
    var properties = exception.GetType()
                            .GetProperties();
    var fields = properties
                     .Select(property => new { 
                         Name = property.Name,
                         Value = property.GetValue(exception, null)
                     })
                     .Select(x => String.Format(
                         "{0} = {1}",
                         x.Name,
                         x.Value != null ? x.Value.ToString() : String.Empty
                     ));
    return String.Join("\n", fields);
}

(Not tested for compliation issues.)

.NET 2.0 compatible answer:

public static string GetExceptionDetails(this Exception exception) 
{
    PropertyInfo[] properties = exception.GetType()
                            .GetProperties();
    List<string> fields = new List<string>();
    foreach(PropertyInfo property in properties) {
        object value = property.GetValue(exception, null);
        fields.Add(String.Format(
                         "{0} = {1}",
                         property.Name,
                         value != null ? value.ToString() : String.Empty
        ));    
    }         
    return String.Join("\n", fields.ToArray());
}
Lee Taylor
  • 7,761
  • 16
  • 33
  • 49
jason
  • 236,483
  • 35
  • 423
  • 525
  • 3
    +1 this is a much better approach than having to override `ToString()` like I suggested. I love extension methods and this seems like a very good approach since we are really wanting to extend the functionality of the base exception class. – Matt Nov 07 '11 at 17:05
  • The alternative to reflection is that i build a class that knows about nearly every kind of exception that i would think i could be interested in. Then i can cast the `Exception` to each kind of known exception, and output the list of properties. – Ian Boyd Nov 07 '11 at 18:50
  • Reflection is likely to be the only answer. Post a .NET 2.0 compatible answer and i'll accept it sooner. Otherwise i'll first have to try my hand at reflection, again. – Ian Boyd Nov 07 '11 at 18:54
  • @Ian Boyd: The reflection part doesn't need to change. See my edit for a .NET 2.0 compatible answer. – jason Nov 07 '11 at 19:10
  • 1
    Instead of reflection, why not just check if the `Exception` in question is a `SqlException`, and if so cast to `SqlException` and read the properties? That will be considerably faster. (While the extra properties are useful in this case, I don't think you can make the general statement that it's *always* useful for every type of exception.) – Daniel Pryden Nov 07 '11 at 19:54
  • @Daniel Pryden: No one cares about performance when an exception is being thrown, or when `GetExceptionDetails` is being called on an instance of `Exception`; this is never performance critical. Also, note the question was not about `SqlException`, but `Exception` in general. That's why. – jason Nov 07 '11 at 19:56
  • @Jason: I won't go so far as to say that *no one* cares about exception performance, but I agree that it's rarely important. But the point I was trying to make was that a reflection-oriented solution presupposes that this problem (that the object contains data that is not represented by `.ToString()`) is a universal one, when in practice it is to my knowledge never an issue outside of `SqlException`. – Daniel Pryden Nov 07 '11 at 19:59
  • @DanielPryden Because i want to be able to handle every kind of exception. This `SqlException` was just the example used in this question. i also want to handle `Win32Exception`, `SocketException`, `XmlException`, any descendants of `ApplicationException`. Basically anywhere `ToString()` and `Message` cause a loss of useful information. Fortunately there's only 2 or 3 dozen exceptions in the FCL with additional properties, so a hard-coded set of exceptions would cause me to take a dependency on only a few dozen .NET dlls. – Ian Boyd Nov 07 '11 at 20:17
  • You can still use the C# 3.0 language features when compiling for .NET 2.0. So there's no need to get rid of `var` etc. The Linq functions are out, of course. That said, it's possible Ian Boyd wants C# 2.0 compatible code, not merely a .NET 2.0 compatible version. – Joren Nov 07 '11 at 21:08
  • 3
    You are forgetting something here. Many times, Exceptions have InnerExceptions which are actually almost more important than the one your get at the top most level. You would need some sort of recursive method to dig deep in in case InnerException is not null. – Pepito Fernandez Jun 15 '13 at 23:22
21

I first tried Jason's answer (at the top), which worked pretty well, but I also wanted:

  • To loop iteratively through inner exceptions and indent them.
  • Ignore null properties and increases readability of the output.
  • It includes the metadata in the Data property. (if any) but excludes the Data property itself. (its useless).

I now use this:

    public static void WriteExceptionDetails(Exception exception, StringBuilder builderToFill, int level)
    {
        var indent = new string(' ', level);

        if (level > 0)
        {
            builderToFill.AppendLine(indent + "=== INNER EXCEPTION ===");                
        }

        Action<string> append = (prop) =>
            {
                var propInfo = exception.GetType().GetProperty(prop);
                var val = propInfo.GetValue(exception);

                if (val != null)
                {
                    builderToFill.AppendFormat("{0}{1}: {2}{3}", indent, prop, val.ToString(), Environment.NewLine);
                }
            };

        append("Message");
        append("HResult");
        append("HelpLink");
        append("Source");
        append("StackTrace");
        append("TargetSite");

        foreach (DictionaryEntry de in exception.Data)
        {
            builderToFill.AppendFormat("{0} {1} = {2}{3}", indent, de.Key, de.Value, Environment.NewLine);
        }

        if (exception.InnerException != null)
        {
            WriteExceptionDetails(exception.InnerException, builderToFill, ++level);
        }
    }

Call like this:

        var builder = new StringBuilder();
        WriteExceptionDetails(exception, builder, 0);
        return builder.ToString();
Gerben Rampaart
  • 9,853
  • 3
  • 26
  • 32
  • 4
    I like your solution @Gerben Rampaart, but it needs some tending. To avoid Exception I replaced `var val = propInfo.GetValue(exception);` with `var val = propInfo!=null?propInfo.GetValue(exception, null):null;` Changed it to private method and used to call it via extension: ` public static string ToStringAllExceptionDetails(this Exception exception) { StringBuilder builderToFill = new StringBuilder(); WriteExceptionDetails(exception, builderToFill, 0); return builderToFill.ToString(); }` – Kraken101 May 22 '15 at 09:31
  • Hi @Kraken101, thanks for your expansion. Feel free to edit my answer to reflect what you're saying. – Gerben Rampaart May 22 '15 at 11:40
  • 1
    @GerbenRampaart WOW! That is such a great solution! Kudos to you! Loved it so much made a new streamlined and minified version for you! See my answer below! :) Thank you for this! :D (and yes I know this is like 10 years later! haha!) – MaxOvrdrv Apr 01 '23 at 04:53
14

This comprehensive answer handles writing out:

  1. The Data collection property found on all exceptions (The accepted answer does not do this).
  2. Any other custom properties added to the exception.
  3. Recursively writes out the InnerException (The accepted answer does not do this).
  4. Writes out the collection of exceptions contained within the AggregateException.

It also writes out the properties of the exceptions in a nicer order. It's using C# 6.0 but should be very easy for you to convert to older versions if necessary.

public static class ExceptionExtensions
{
    public static string ToDetailedString(this Exception exception)
    {
        if (exception == null)
        {
            throw new ArgumentNullException(nameof(exception));
        }

        return ToDetailedString(exception, ExceptionOptions.Default);
    }

    public static string ToDetailedString(this Exception exception, ExceptionOptions options)
    {
        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. If you are using .NET Core 2.1, then this feature comes built in.

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

For people who don't want to mess with overriding, this simple non-intrusive method might be enough:

    public static string GetExceptionDetails(Exception exception)
    {
        return "Exception: " + exception.GetType()
            + "\r\nInnerException: " + exception.InnerException
            + "\r\nMessage: " + exception.Message
            + "\r\nStackTrace: " + exception.StackTrace;
    }

It does not show the SQLException-specific details you want, though...

Nicolas Raoul
  • 58,567
  • 58
  • 222
  • 373
6

There is no secret method. You could probably just override the ToString() method and build the string you want.

Things like ErrorCode and Message are just properties of the exception that you can add to the desired string output.


Update: After re-reading your question and thinking more about this, Jason's answer is more likely what you are wanting. Overriding the ToString() method would only be helpful for exceptions that you created, not already implemented ones. It doesn't make sense to sub class existing exceptions just to add this functionality.

Community
  • 1
  • 1
Matt
  • 14,353
  • 5
  • 53
  • 65
  • Yeah, i can't really override `System.Data.SqlClient.SqlException` and make `System.Data.SqlConnection` use it. – Ian Boyd Nov 07 '11 at 18:52
4

For displaying some details to user you should use ex.Message. For displaying to developers you will probably need ex.Message and ex.StackTrace.

There is no 'secret' method, you could consider Message property to be best fit for user friendly message.

Also be careful that in some case you may have inner exception in exception you catch which would be also useful to log.

Nikola Radosavljević
  • 6,871
  • 32
  • 44
2

Each left-side name is property in the Exception. If you want to display Message field, you can do

return ex.Message;

Pretty simple. Likewise, the StackTrace can be displayed as below link.

A complete example of StackTrace: http://msdn.microsoft.com/en-us/library/system.exception.stacktrace.aspx

and Exception class: http://msdn.microsoft.com/en-us/library/system.exception.aspx

Youngjae
  • 24,352
  • 18
  • 113
  • 198
2

You will probably have to manually construct that string by concatenating the various fields you are interested in.

Tudor
  • 61,523
  • 12
  • 102
  • 142
1

I think the exception serialization to JSON is nice option. Sample result:

{
"Errors": [{
        "Source": ".Net SqlClient Data Provider",
        "Number": -1,
        "Class": 20,
        "Server": "111.168.222.70",
        "Message": "A transport-level error has occurred when receiving results from the server. (provider: Session Provider, error: 19 - Physical connection is not usable)"
    }
],
"ClientConnectionId": "b1854037-51e4-4943-94b4-72b7bb4c6ab7",
"ClassName": "System.Data.SqlClient.SqlException",
"Message": "A transport-level error has occurred when receiving results from the server. (provider: Session Provider, error: 19 - Physical connection is not usable)",
"Data": {
    "HelpLink.ProdName": "Microsoft SQL Server",
    "HelpLink.EvtSrc": "MSSQLServer",
    "HelpLink.EvtID": "-1",
    "HelpLink.BaseHelpUrl": "http://go.microsoft.com/fwlink",
    "HelpLink.LinkId": "20476"
},
"InnerException": null,
"HelpURL": null,
"StackTraceString": "   at System.Data.SqlClient.SqlConnection.OnError ... DbExecutionStrategy.Execute[TResult](Func`1 operation)",
"RemoteStackTraceString": null,
"RemoteStackIndex": 0,
"ExceptionMethod": "8\nOnError\nSystem.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\nSystem.Data.SqlClient.SqlConnection\nVoid OnError(System.Data.SqlClient.SqlException, Boolean, System.Action`1[System.Action])",
"HResult": -2146232060,
"Source": ".Net SqlClient Data Provider",
"WatsonBuckets": null
}
0

In visual studio that sort of information can be outputted by a debugger visualizer.

I assume that because it is possible to write your own debugger visualizer: http://msdn.microsoft.com/en-us/library/e2zc529c.aspx

That in theory, if your can reverse engineer the built-in debugger visualizer for exceptions (if your can work out where they are stored) then you could use the same functionality.

EDIT:

Here is a post about where the debugger visualizers are kept: Where do I find Microsoft.VisualStudio.DebuggerVisualizers?

You might be able to use it for your own purposes.

Community
  • 1
  • 1
Alex KeySmith
  • 16,657
  • 11
  • 74
  • 152
0

If you call ToString on Exception object, you get the class name appended by the message, followed by inner exception and then the stack trace.

className + message + InnerException + stackTrace

Given that, InnerException and StackTrace are only added if they are not null. Also, the fields you have mentioned in the screenshot are not part of standard Exception class. Yes, exception does offer a public property called "Data", that contain additional user-defined information about the exception.

Pawan Mishra
  • 7,212
  • 5
  • 29
  • 39