24

Everytime I need to provide additional information about an exception I wonder which way is actually the right way of doing this.


For the sake of this question I wrote an example. Let's assume there is a class where we want to update the Abbreviation property. From the SOLID point of view it might not be perfect but even if we passed the worker-method via DI with some service the same situation would occur - an exception occurs an there is no context to it. Back to the example...

class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Abbreviation { get; set; }
}

Then there are some instances of the class and a loop where the worker-method is called. It can throw the StringTooShortException.

var persons =
{
    new Person { Id = 1, Name = "Fo" },
    new Person { Id = 2, Name = "Barbaz" },
}

public IEnumerable<Person> GenerateAbbreviation(IEnumerable<Person> persons)
{
    foreach (var person in persons)
    {
        try
        {
            person.Abbreviation = GenerateAbbreviation(person.Name);
        }
        catch(Exception ex)
        {
            // ?
        }
    }
    // throw AggregateException...
}

public IEnumerable<string> GenerateAbbreviation(string value)
{
    if (value.Length < 5)
    {
        throw new StringTooShortException(value);
    }

    // generate abbreviation
}

The quesion is: how to add the Person or its Id (or anything else)?


I know the following three techniques:


1 - Use the Data property

Pros:

  • easy to set additional information
  • does not require creating even more exceptions
  • does not require additional try/catch

Cons:

  • cannot be easily integrated into the Message
  • loggers ignore this field and won't dump it
  • requires keys and casting becasue values are object
  • not immutable

Example:

public IEnumerable<Person> GenerateAbbreviation(IEnumerable<Person> persons)
{
    foreach (var person in persons)
    {
        try
        {
            person.Abbreviation = GenerateAbbreviation(person.Name);
        }
        catch(Exception ex)
        {
            ex.Data["PersonId"] = person.Id;
            // collect ex
        }
    }
    // throw AggregateException...
}

2 - Use custom properties

Pros:

  • similar to the Data property but strongly typed
  • easier to integrate into the Message

Cons:

  • requires custom exceptions
  • logger will ignore them
  • not immutable

Example:

public IEnumerable<Person> GenerateAbbreviation(IEnumerable<Person> persons)
{
    foreach (var person in persons)
    {
        try
        {
            person.Abbreviation = GenerateAbbreviation(person.Name);
        }
        catch(Exception ex)
        {
            // not suitable for this exception because 
            // it doesn't have anything in common with the Person
        }
    }
    // throw AggregateException...
}

3 - Wrap the exception with another exception

Pros:

  • Message can be formatted in a predictable way
  • loggers will dump inner exceptions
  • immutable

Cons:

  • requires additional try/catch
  • increses nesting
  • increaes the depth of the exeptions

Example:

public IEnumerable<Person> GenerateAbbreviation(IEnumerable<Person> persons)
{
    foreach (var person in persons)
    {
        try
        {
            try
            {
                person.Abbreviation = GenerateAbbreviation(person.Name);
            }
            catch(Exception ex)
            {
                throw new InvalidPersonDataException(person.Id, ex);
            }
        }
        catch(Exception ex)
        {
            // collect ex
        }
    }
    // throw AggregateException...
}

  • Are there any other patterns?
  • Are there better patterns?
  • Can you suggest best practices for any/all of them?
7
  • Not familiar with exceptions in C# but I would normally expect the Person instance still to be valid when the exception is thrown. Have you tried that? Commented Nov 19, 2016 at 21:48
  • 1
    @JohnKouraklis this is not what the question is about ;-) It's just an extremely simple example to demonstrate what I mean by additional information. If I posted here an entire framework where mutliple methods can throw exceptions and mutliple levels of context information should be provided no one probably would read this and I had a really hard time to explain it.
    – t3chb0t
    Commented Nov 19, 2016 at 21:53
  • @JohnKouraklis I've just made it up for demonstration purposes.
    – t3chb0t
    Commented Nov 19, 2016 at 21:55
  • @t3chb0t I think you've answered your own question here. Consider moving 1, 2, & 3 into an answer and adjusting your question so it's not asking me to pick a style based on my opinion. Commented Nov 20, 2016 at 5:57
  • 1
    I have been waiting over a decade to see a .NET application actually provide information in the Data dictionary. I am still waiting . . . I guess nobody logs it, so no one expects it to get read, so nobody writes in it?
    – Mike
    Commented Dec 2, 2016 at 15:46

4 Answers 4

7

Data FTW.

Your "contra":

  • "cannot be easily integrated into the Message"

-> For your exception types, it should be easy enough to override Messageso that it does incorporate Data .. although I only would consider this if the Data is the message.

  • "loggers ignore this field and won't dump it"

Googling for Nlog as an example yields:

Exception layout renderer

(...)

format - Format of the output. Must be a comma-separated list of exception properties: Message, Type, ShortType, ToString, Method, StackTrace & Data. This parameter value is case-insensitive. Default: message

So it seems that's easily configurable.

  • requires keys and casting because values are object

Huh? Just dump the objects in there and make sure they have a usable ToString() method.

Also, I don't quite see any problem with the keys. Just use some mild uniqueness and you're good.


Disclaimer: This is what I could immediately see from the question and what I googled up on Data in 15 minutes. I thought it's mildly helpful, so I put it out as an answer, but I have never used Data myself, so it might well be that the questioner here knows way more about this than me.

1
  • I came to the conclusion that there are only two things about an exception that are useful, its name and message. Everything else is just useless noise that can and should be ignored because it's simply too fragile rely on it.
    – t3chb0t
    Commented Feb 15, 2019 at 11:01
4

Why do you throw Exceptions? To have them caught and handled.

How does the catching code work out how to handle the Exception? Using the properties that you define on the Exception object.

Never use the Message property to identify the exception, nor to provide "information" that any potential handler should rely on. It's simply too volatile and unreliable.

I've never used the "Data" property before but it sounds overly generic to me.

Unless you create many classes of Exception, each of which identifies a specific Exceptional case, how do you know when you catch the Exception what "Data" represents? (See preceding comment about "Message").

1
  • 2
    I'd say, Datais useless for handling, but valuable for logging to avoid Message formatting hell.
    – Martin Ba
    Commented Nov 24, 2016 at 18:47
0

As a general rule the exception message should be a static string that doesn't change depending on circumstances. Any information that is useful to have and varies between calls should go into the Data dictionary. Loggers are able to handle this as has been mentioned. The rules concerning the exception message are the same as those concerning logging message.

This extension method can be used to easily add data to an exception before throwing:

public static Exception AddData(this Exception ex, string key, object value)
{
  ex.Data.Add(key, value);
  return ex;
}

i.e.

throw new CustomException("Failed").AddData("key", val);
-1

I like your third example, however there is another way it can be coded to eliminate most of your "con"s.

public IEnumerable<Person> GenerateAbbreviation(IEnumerable<Person> persons)
{
    var exceptions = new List<InvalidPersonDataException>();

    foreach (var person in persons)
    {
        try
        {
            person.Abbreviation = GenerateAbbreviation(person.Name);
        }
        catch(Exception ex)
        {
            exceptions.Add(new InvalidPersonDataException(person.Id, ex));
        }
    }

    if (exceptions.Any())
    {
        throw new AggregateException(exceptions);
    }
}

Not the answer you're looking for? Browse other questions tagged or ask your own question.