Defining Custom Exceptions

Once throwing an exception becomes the best course of action, it is preferable to use framework exceptions because they are well established and understood. Instead of throwing a custom invalid argument exception, for example, it is preferable to use the System.ArgumentException type. However, if the developers using a particular API will take special action—the exception-handling logic will vary to handle a custom exception type, for instance—it is appropriate to define a custom exception. For example, if a mapping API receives an address for which the ZIP code is invalid, instead of throwing System.ArgumentException, it may be better to throw a custom InvalidAddressException. The key is whether the caller is likely to write a specific InvalidAddressException catch block with special handling rather than just a generic System.ArgumentException catch block.

Defining a custom exception simply involves deriving from System.Exception or some other exception type. Listing 11.4 provides an example.

Listing 11.4: Creating a Custom Exception
class DatabaseException : Exception
{
    public DatabaseException(
        string? message,
        System.Data.SqlClient.SQLException? exception)
        : base(message, innerException: exception)
    {
        // ...
    }
 
    public DatabaseException(
        string? message,
        System.Data.OracleClient.OracleException? exception)
        : base(message, innerException: exception)
    {
        // ...
    }
 
    public DatabaseException()
    {
        // ...
    }
 
    public DatabaseException(string? message)
        : base(message)
    {
        // ...
    }
 
    public DatabaseException(
        string? message, Exception? exception)
        : base(message, innerException: exception)
    {
        // ...
    }
    // ...
}

This custom exception might be created to wrap proprietary database exceptions. Since Oracle and SQL Server (for example) throw different exceptions for similar errors, an application could define a custom exception that standardizes the database-specific exceptions into a common exception wrapper that the application can handle in a standard manner. That way, whether the application was using an Oracle or a SQL Server back-end database, the same catch block could be used to handle the error higher up the stack.

The only requirement for a custom exception is that it derives from System.Exception or one of its descendants. Other good practices for custom exceptions are as follows:

All exceptions should use the “Exception” suffix. This way, their purpose is easily established from their name.
Generally, all exceptions should include constructors that take no parameters, a string parameter, and a parameter set consisting of a string and an inner exception. Furthermore, since exceptions are usually constructed within the same statement in which they are thrown, any additional exception data should be allowed as part of the constructor. (The obvious exception to creating all these constructors is if certain data is required and a constructor would circumvent the requirements.)
The inheritance chain should be kept relatively shallow (with fewer than approximately five levels).

The inner exception serves an important purpose when rethrowing an exception that is different from the one that was caught. For example, if a System.Data.SqlClient.SqlException is thrown by a database call but is caught within the data access layer and will be rethrown as a DatabaseException, the DatabaseException constructor that takes the SqlException (or inner exception) will save the original SqlException in the InnerException property. That way, if they require additional details about the original exception, developers can retrieve the exception from the InnerException property (e.g., exception.InnerException).

Guidelines
AVOID deep exception hierarchies.
DO NOT create a new exception type if the exception would not be handled differently than an existing CLR exception. Throw the existing framework exception instead.
DO create a new exception type to communicate a unique program error that cannot be communicated using an existing CLR exception and that can be programmatically handled in a different way than any other existing CLR exception type.
DO provide a parameterless constructor on all custom exception types. Also provide constructors that take a message and an inner exception.
DO name exception classes with the “Exception” suffix.
DO make exceptions runtime-serializable.
CONSIDER providing exception properties for programmatic access to extra information relevant to the exception.
AdVanced Topic
Serializable Exceptions

Serializable objects are objects that the runtime can persist into a stream—a file stream, for example—and that can then be reinstantiated out of the stream. In the case of exceptions, this behavior may be necessary for certain distributed communication technologies. To support serialization, exception declarations should either include the System.SerializableAttribute attribute or implement ISerializable. Furthermore, they must include a constructor that takes System.Runtime.Serialization.SerializationInfo and System.Runtime.Serialization.StreamingContext. Listing 11.5 shows an example of using System.SerializableAttribute.

Listing 11.5: Defining a Serializable Exception
// Supporting serialization via an attribute
[Serializable]
class DatabaseException : Exception
{
    // ...
 
   // Used for deserialization of exceptions
   public DatabaseException(
       SerializationInfo serializationInfo,
       StreamingContext context)
       : base(serializationInfo, context)
    {
        //...
    }
}

The preceding DatabaseException example demonstrates both the attribute and the constructor requirement for making an exception serializable.

For .NET Core, System.SerializableAttribute was not available until .NET Standard 2.0. If you are writing code that will compile across multiple frameworks including a .NET Standard version prior to 2.0, consider defining your own System.SerializableAttribute as a polyfill. A polyfill is code that fills a hole in a particular version of technology, thereby adding the functionality or at least providing a shim for what is missing.

{{ snackbarMessage }}
;