C# Corner

Working Effectively with Exceptions

Exceptions are a way of life in the .NET world. You must follow the rules to make your classes easy to use for other developers. Conforming to the standards will make everyone's life easier.

The .NET world has decided that exceptions are the way to report and propagate errors. Error codes, HRESULTs and other less-intrusive mechanisms are relegated to the past. They were easily ignored, causing cascading application errors. When users finally realized an application wasn't working correctly, data had been lost, and it was too late to recover lost work. Exceptions can't be ignored. If you try, your application will just terminate.

This article will give you a series of guidelines for when to throw exceptions, when to catch exceptions and how to avoid impairing performance because of exceptions.

The ".NET Framework Design Guidelines" (Abrams & Cwalina, Addison Wesley, 2009) declares that, "If a member cannot successfully do what it is designed to do, it should be considered an execution failure, and an exception should be thrown" (p. 218). Either a member fulfills its contract or it throws an exception. Period. No middle ground. No return codes, no sometimes it works, sometimes it doesn't. Either a member does what it claims to do or it throws an exception.

That one guideline has big implications for how you design your APIs. You want to design an API that minimizes the chance that your members cannot fulfill their contract. You don't want to force users of your classes to introduce numerous catch clauses in order to work with your classes. Instead, you want to design an API that's easy to use correctly and hard to misuse to the point of an execution failure.

Let's take a look at what I mean by creating a new utility class and working to create an API that minimizes the chance of throwing exceptions. Here's a first pass at a Task class for a project management application:

public class Task
{
    public DateTime Start
    {
        get;
        set;
    }
    public DateTime Due
    {
        get;
        set;
    }
    public string Title
    {
        get;
        set;
    }
    private List<string> notes = new List<string>();
    public IEnumerable<string> Notes
    {
        get { return notes; }
    }

    public void AddNote(string note)
    {
        notes.Add(note);
    }
}

As currently coded, this class never throws any exceptions. The get and set accessors for all the properties are simple pass-through accessors. AddNote won't throw any exceptions. The enumeration won't throw an exception, no matter what values of strings you place in the list of notes. That sounds like you did exactly what you need to do to avoid having users worry about contract failures. Your class never throws an exception, so you don't have any issues with execution failures.

But a closer look changes that rosy picture. Classes that use this will need to perform extensive validation against the data stored in a Task. The default constructor creates a Task that's probably invalid: the Start- and Due-date time objects contain the default bit pattern of zeroes. That's meaningless. The title is a null string. The only property that's valid is the notes: there's an empty container, meaning this Task doesn't contain any notes.

That means this version of the Task class won't throw exceptions, but only because it's a dumb data container, devoid of behavior. You've just passed the buck to other developers that will need to devise their own conventions to handle any deficiencies stored in a Task object. For example, how would you compute the planned duration of a task? That would likely be the difference between the Due date and the Start date. As coded, that could be a zero-length duration, or even a negative duration. There may be too many times when we are asked to finish a task yesterday, but even though our managers may wish it, it's still impossible.

Other problems may be more insidious: Could you find a Task by name? What would happen for null names? How about empty names? What about duplicate names? When you define an API for a class, you need to consider how it will be used. In addition to minimizing the exceptions for that class, you need to ask yourself if your API simply pushes hard questions on to other classes. That doesn't help anyone. Let's update this class so that it lives up to users' expectations and avoids forcing client code to handle exceptions in too many ways.

As always, there are many ways to fix these issues. I've picked one possible way to address these issues. Let's begin with some assumptions. Most developers would assume that the Due date comes after the Start date, and that a Task must have a non-empty title. After all, that's how you'll search for a Task. Finally, let's assume that a Task must be XML Serializable, which implies that it must have a default constructor. That last assumption complicates the problem, but is just the kind of real-world requirement that often complicates designs.

If not for the default constructor requirement, we would simply convert the properties to read only properties, and initialize them in a single constructor. The code would look something like this:

public class Task
{
    public DateTime Start
    {
        get;
        private set;
    }
    public DateTime Due
    {
        get;
        private set;
    }
    public string Title
    {
        get;
        private set;
    }
    private List<string> notes = new List<string>();
    public IEnumerable<string> Notes
    {
        get { return notes; }
    }

    public void AddNote(string note)
    {
        notes.Add(note);
    }
    // Added:
    public Task(string title, DateTime start, 
	 	  DateTime due)
    {
        // Validate title:
        if (string.IsNullOrEmpty(title))
            throw new ArgumentException(
            "Title cannot be empty", "title");

        // validate start, due:
        if (start == new DateTime())
            throw new ArgumentException(
            "Default DateTime is not a valid start date", 
	         "start");
        if (due == new DateTime())
            throw new ArgumentException(
            "Default DateTime is not a valid due date",
            "due");
        if (start >= due)
            throw new InvalidOperationException(
            "Task cannot be due before it starts");

        Title = title;
        Start = start;
        Due = due;
    }
}

This version satisfied all the assumptions I wrote above, except for the requirement to support XML Serialization. Even so, I want to examine this approach a bit more in-depth because it's a common practice. Here, I've changed the contract so that the title, the Start date and the End date must be specified as parameters to the constructor, and can never be changed. That greatly simplifies exception handling: a Task object can never enter an invalid state. Once constructed, it's valid, and there's no way to mutate a Task to an invalid state. If you can ensure that (even more so if you can create an immutable type), it's much easier to minimize the ways in which your type will throw exceptions.

The lack of a default constructor means this version of the Task class can't be serialized using XML Serialization, which violates the requirements, so you must pick a different solution. XML Serialization requires that the Task type have a parameterless constructor, and that all the properties are read/write. That means there are a number of changes you need to make. You must make all the properties read/write, including the collection of notes. You must allow the default state to be valid, even if later mutations must be validated against a possible invalid state. These requirements and their implications will change how you think about exceptions, as suddenly it's not easy to keep the correct state of the object at all times. That's why I picked a class like this as a sample.

The conflicting requirements mean that you must think hard about when something is valid and invalid. Let's walk through a series of enhancements and determine how to update the Task class so that you throw exceptions only when the object's member can't fulfill its contract. Achieving this involves defining the contracts and object invariants carefully. You can begin by adding the default constructor, which will force you to address all the other validation issues. Can you define reasonable default values for a new object? Here, you can make some invariants for a Task. You can enforce a non-empty name. You can initialize the Start date and the Due date to safe values:

public Task() :
    this("default", 
        DateTime.Now, 
        DateTime.Now + 	    	      
	TimeSpan.FromDays(365*5))
{
}

The default name is a reasonable starting point, but the date time values feel like a code smell. The goal of picking the Start date and End date with such a long lag is to ensure that setting either property ensures that the end date is still later than the start date. However, the fact remains that for almost all possible default values of the Start date and End date, it's possible to violate the object invariant. Instead, let's change the public properties to make it easier to enforce the contract. Instead of storing a Due date, you should store the duration for the Task, and compute the Due date from the Start date.

You can change the parameters to the constructor as well to make the public API more consistent. Here's the updated Task class:

public class Task
{
    public DateTime Start
    {
        get;
        private set;
    }
    public TimeSpan Duration
    {
        get;
        private set;
    }
    public DateTime Due
    {
        get
        {
            return Start + Duration;
        }
    }
    public string Title
    {
        get;
        private set;
    }
    private List<string> notes = new List<string>();
    public IEnumerable<string> Notes
    {
        get { return notes; }
    }

    public void AddNote(string note)
    {
        notes.Add(note);
    }
    // Added:
    public Task(string title, DateTime start, 
        TimeSpan duration)
    {
        // Validate title:
        if (string.IsNullOrEmpty(title))
            throw new ArgumentException(
            "Title cannot be empty", "title");

        // validate start, due:
        if (start == new DateTime())
            throw new ArgumentException(
            "Default DateTime is not a valid start date",
            "start");
        if (duration <= new TimeSpan())
            throw new InvalidOperationException(
            "Task cannot be due before it starts");

        Title = title;
        Start = start;
        Duration = duration;
    }

    public Task() :
        this("default", 
        DateTime.Now, 
        TimeSpan.FromDays(1))
    {
    }
}

That's what I meant about modifying the API to make it easier to enforce contracts. It will be difficult to enforce a constraint that the Due date is after the Start date if the class supports read/write properties for both. That's because there's a dependency between the two properties. Client code must be careful about the order of modifying these two related properties. You've made this simpler by creating two different properties that can be modified independently.

You'll see this more clearly as you add the validation code for each of the properties. Let's do that now. Modify the implicit properties to create explicit backing fields. Then, add validation on each of the set accessors to enforce the constraints that match your business rules. Here are the updated read/write properties:

private DateTime start;
public DateTime Start
{
    get { return start; }
    set { start = value; }
}

private TimeSpan duration;
public TimeSpan Duration
{
    get { return duration; }
    set
    {
        if (duration <= new TimeSpan())
            throw new ArgumentException(
            "You cannot create a negative duration",
            "value");
        duration = value;
    }
}
private string title;
public string Title
{
    get { return title; }
    set
    {
        if (string.IsNullOrEmpty(value))
            throw new ArgumentException(
            "Task title cannot be blank", "value");
        title = value;
    }
}

Now you can more clearly see the advantage of de-coupling the relationships between the different properties. The validation code for any property is independent of the other properties.

That covers some of the basics on creating a class that enforces its contracts using exceptions. Setting a property to an invalid value would violate the object invariants. The only way to report that error is to throw an exception. A member must either fulfill its contract (setting a new value) or throw an exception.

Catching and Fixing
I spent more space on throwing than catching exceptions because there are more design decisions that you have to make on what and when to throw exceptions. However, if contract failures are reported using exceptions, you need to write catch clauses somewhere to keep your application running. Uncaught exceptions terminate your application. This draconian measure causes too many developers to implement this common anti-pattern:

try
{
    DoSomething();
}
catch (Exception e)
{
    Logstuff();
}

I can't stress this enough: Catching System.Exception is a terrible practice. You'll end up catching exceptions like the BadImageFormatException, ExecutionEngineException, AccessViolationException or the InvalidProgramException. You can't recover from those exceptions. The common language runtime probably can't recover from those exceptions and the only prudent course of action is to close the application before more catastrophic data loss occurs.

All your code should follow these guidelines: strive for a path that avoids exceptions in reasonable actions. Only check those exceptions from which you can recover, and let the rest propagate up the call stack. Obviously, you don't want to show your users the standard developer exception messages (from desktop or Web). That doesn't mean you should catch every exception. Instead, you can set up a global exception handler to process unhandled exceptions, inform your users about the problem and stop the application gracefully.

The .NET Base Class Library (BCL) follows the guidelines I referenced at the beginning of this article: A member either satisfies its stated contract or it throws an exception. Your code will be easier to maintain if you follow those same guidelines. It follows from those guidelines that you will want to define an API that enables your users to avoid triggering exceptional conditions under most normal usage scenarios. Exceptions indicate the failure of a member to do what was intended, so swallowing System.Exception almost surely leads to an unstable application. You should avoid that bad practice in your code whenever possible.

About the Author

Bill Wagner, author of Effective C#, has been a commercial software developer for the past 20 years. He is a Microsoft Regional Director and a Visual C# MVP. His interests include the C# language, the .NET Framework and software design. Reach Bill at [email protected].

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube