.NET has a large number of built in exceptions. However, there maybe times when none of the built exceptions seem adequate for your particular scenario and you will need to create your own custom (AKA “user defined”) exception.
This post focuses on a discussion of how to create custom exceptions in .NET. The different types of built in .NET exceptions, exception handling, throwing, and more general exception related best practices will not be covered.
All code examples are in C#.
Basic Custom Exception Example
The following example illustrates the most basic template that every custom exception should follow:
using System;
using System.Runtime.Serialization;
// ...
[Serializable]
public class EntityNotFoundException : Exception
{
private const string DefaultMessage = "Entity does not exist.";
public EntityNotFoundException() : base(DefaultMessage)
{
}
public EntityNotFoundException(string message) : base(message)
{
}
public EntityNotFoundException(string message, Exception innerException) : base(message, innerException)
{
}
protected EntityNotFoundException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
Example notes:
- The class inherits from Exception. Custom exceptions should not inherit from ApplicationException. In the early days of .NET Microsoft actually recommended inheriting from ApplicationException but has since changed that recommendation.
- The class is decorated with the Serializable attribute. Custom exceptions are not serializable by default. But making a custom exception serializable allows it to be properly marshalled across app domains and threads. It should be noted that simply decorating your custom exception with Serializable is not enough to serialize any extra properties you might have defined. More on this in the “Extended Custom Exception Example” section below.
- The class should always (unless there is a very good reason) have the four Microsoft recommended constructors: public (), public (string message), public (string message, Exception innerException), protected (SerializationInfo info, StreamingContext context).
- The custom exception class itself is not marked as implementing ISerializable. Doing so would be redundant as we are inheriting from Exception which itself implements ISerializable.
- We provide a default message that makes sense for the custom exception when the parameterless constructor is called.
- The custom exception’s name ends with “Exception”. All exceptions in .NET should have the name suffix of “Exception”.
- Any XML documentation comments have not been added for the sake of brevity. However, adding them to the public interface of the exception can be a good idea to aid in it’s use.
Extended Custom Exception Example
In the following example we build upon our basic example from the previous section and add the concept of adding our own property. In this case an integer property EntityId:
using System;
using System.Runtime.Serialization;
// ...
[Serializable]
public class EntityNotFoundException : Exception
{
private const string DefaultMessage = "Entity does not exist.";
public int EntityId { get; }
public EntityNotFoundException() : base(DefaultMessage)
{
}
public EntityNotFoundException(string message) : base(message)
{
}
public EntityNotFoundException(string message, Exception innerException) : base(message, innerException)
{
}
public EntityNotFoundException(int entityId) : this($"Entity does not exist with ID: '{entityId}'.")
{
EntityId = entityId;
}
protected EntityNotFoundException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
EntityId = (int)info.GetValue(nameof(EntityId), typeof(int));
}
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue(nameof(EntityId), EntityId, typeof(int));
}
}
Example notes:
- Additional new properties, in this case EntityId, should not have non-private setters. This is because .NET exceptions should be immutable (once created their state should not change). Instead all values should be passed through and set in the constructor.
- A new method has been added: GetObjectData. This method is part of the ISerializable interface which Exception implements. When overridden it allows us to set extra information that we want to save during serialization. In this case the integer value in our property EntityId. When overriding this method make sure to also call the base implementation.
- Our protected constructor which was previously empty now has implementation to help set our new EntityId property when our exception is being deserialized.
Unit Testing Custom Exceptions
We can unit test our extended custom exception example with the following tests:
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using NUnit.Framework;
// ...
[TestFixture]
public class EntityNotFoundExceptionTests
{
private const string Message = "some message";
private const int EntityId = 1;
[Test]
public void WhenNoArgs_ThenSetMessageToDefault()
{
var sut = new EntityNotFoundException();
Assert.That(sut.Message, Is.EqualTo("Entity does not exist."));
}
[Test]
public void WhenMessageSpecified_ThenSetMessage()
{
var sut = new EntityNotFoundException(Message);
Assert.That(sut.Message, Is.EqualTo(Message));
}
[Test]
public void WhenMessageAndInnerExSpecified_ThenSetMessageAndInnerEx()
{
var innerException = new Exception();
var sut = new EntityNotFoundException(Message, innerException);
Assert.That(sut.Message, Is.EqualTo(Message));
Assert.That(sut.InnerException, Is.SameAs(innerException));
}
[Test]
public void WhenIdSpecified_ThenSetProperty()
{
var sut = new EntityNotFoundException(EntityId);
Assert.That(sut.Message, Is.EqualTo($"Entity does not exist with ID: '{EntityId}'."));
Assert.That(sut.EntityId, Is.EqualTo(EntityId));
}
[Test]
public void WhenSerialized_ThenDeserializeCorrectly()
{
var sut = new EntityNotFoundException(EntityId);
var result = Serializer.SerializeAndDeserialize(sut);
Assert.That(result.EntityId, Is.EqualTo(sut.EntityId));
Assert.That(result.ToString(), Is.EqualTo(sut.ToString()));
}
}
// ...
internal static class Serializer
{
public static TException SerializeAndDeserialize<TException>(TException exception)
{
var formatter = new BinaryFormatter();
using (var stream = new MemoryStream())
{
formatter.Serialize(stream, exception);
stream.Seek(0, 0);
return (TException)formatter.Deserialize(stream);
}
}
}
The tests cover five scenarios: the four different constructors and what properties they set. The fifth test checks the serialization/deserialization of the exception and it’s EntityId property value.
Final Thoughts
So when should we create our own custom exception instead of using a built in .NET exception? If a .NET built in exception fits your requirement then you should certainly use it. However, if you do go down the route of creating your own custom exception here are some things to bear in mind:
- Don’t create custom exceptions for every type of situation. Be wary if a custom exception is too granular. Instead generalize to one custom exception and be more specific in the exception’s message when the exception is thrown.
- Don’t create complex hierarchies of custom exceptions for your application. Most of the time it will make the most sense that your custom exception inherits directly from Exception.
- Do create a custom exception to signify a problem in your particular application domain. For example creating a custom exception that represents that a problem has occurred in a NuGet package library you have created.
- When creating custom exceptions do think about the exception handling that a consumer of your code might have to deal with. For example if you create a number of different custom exceptions is the consumer of your code going to have a number of different catch blocks? It might also be worth considering how you are going to communicate that your exception exists and under what scenarios it will be thrown to the consumer of your code.
As of writing .NET is nearly 20 years old and there are many posts on the internet about creating custom exceptions. However, virtually all only cover small parts of the topic or unfortunately give bad advice. Hopefully this post has provided a more complete picture on how to create custom exceptions.