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.
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:
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).
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.
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.