C# Exceptions – How to Provide Additional Information About an Exception

cexceptions

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?

Best Answer

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.

Related Topic