Programming

Make a custom NET Exception serializable

25 September 2026 · 11 min read

Make a custom NET Exception serializable

Handling exceptions effectively is crucial for building robust and maintainable .NET applications. When dealing with distributed systems or scenarios requiring serialization, ensuring your custom exceptions are serializable becomes paramount. This allows you to properly log, transport, and handle errors across different application tiers or even across the network. Failing to implement proper serialization can lead to data loss and hinder debugging efforts when exceptions occur in remote parts of your system. This article will guide you through the process of making custom .NET exceptions serializable, ensuring you can effectively manage errors in any environment.

Understanding Exception Serialization in .NET

Serialization is the process of converting an object into a stream of bytes for storage or transmission. When an exception is thrown, crucial information about the error, such as the message, stack trace, and inner exceptions, is encapsulated within the exception object. To preserve this information when serializing an exception, specific attributes and interfaces need to be implemented.

The core of .NET exception serialization relies on the [Serializable] attribute and, optionally, the ISerializable interface. Applying the [Serializable] attribute marks your custom exception class as serializable, allowing the .NET framework to handle the basic serialization process. Implementing the ISerializable interface provides finer control over the serialization process, letting you specify exactly which members of your exception class should be serialized.

Ignoring serialization can lead to critical information loss when handling exceptions in distributed environments. Imagine an exception occurring in a background service that communicates with your main application. Without proper serialization, the details of the exception might be lost during transmission, hindering debugging efforts.

Implementing the Serializable Attribute

The simplest way to make a custom exception serializable is by decorating the class with the [Serializable] attribute. This tells the .NET runtime that instances of this class can be converted into a byte stream.

C [Serializable] public class CustomException : Exception { // … (your custom exception properties and methods) }

This approach is sufficient for many scenarios where the default serialization behavior is adequate. The framework will automatically serialize all public and private fields of the exception. However, for more complex scenarios requiring custom serialization logic, the ISerializable interface provides greater control.

Implementing the ISerializable Interface

For finer-grained control over the serialization process, implement the ISerializable interface. This interface requires you to implement the GetObjectData method, which allows you to specify which members of your exception should be serialized. This method receives a SerializationInfo object, which you populate with the data you want to serialize.

C [Serializable] public class CustomException : Exception, ISerializable { // … (your custom exception properties) public CustomException() { } public CustomException(string message) : base(message) { } public CustomException(string message, Exception inner) : base(message, inner) { } protected CustomException(SerializationInfo info, StreamingContext context) : base(info, context) { // Deserialize custom properties here CustomProperty = info.GetString(“CustomProperty”); } public void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); // Serialize custom properties here info.AddValue(“CustomProperty”, CustomProperty); } public string CustomProperty { get; set; } }

This example demonstrates how to serialize a custom property named CustomProperty. Remember to call the base implementation of GetObjectData to ensure the standard exception properties are also serialized. This meticulous approach guarantees that all necessary data is preserved during serialization, even for complex exception scenarios.

Best Practices for Serializable Exceptions

Following best practices ensures your custom serializable exceptions are efficient and maintainable. Avoid serializing unnecessary data to keep the serialized object size small. Carefully consider which exception properties are essential for debugging and error handling. Documenting the serialized data is crucial for developers who may need to deserialize and interpret the exception information.

  • Only serialize necessary data.
  • Clearly document the serialized data.

Consider versioning your exceptions to maintain compatibility across different application versions. If the structure of your exception changes, ensure backward compatibility by handling older serialized versions. Implementing these best practices will contribute to a more robust and maintainable error handling strategy in your applications.

  1. Apply the [Serializable] attribute.
  2. Implement ISerializable for custom serialization.
  3. Test your serialized exceptions thoroughly.

Testing your serialized exceptions rigorously in various scenarios is crucial to validate their functionality. Simulate different exception scenarios and ensure the serialized data can be successfully deserialized and interpreted. Thorough testing will give you confidence that your exception handling mechanism is reliable and effective.

Serialization plays a vital role in maintaining application stability. By implementing proper serialization for custom exceptions, developers can ensure critical error information is preserved, facilitating debugging and error handling in distributed environments. Neglecting exception serialization can lead to information loss and hinder troubleshooting efforts.

Real-World Example: Logging Exceptions in a Distributed System

Imagine a distributed system where a microservice throws a custom exception. This exception needs to be logged in a centralized logging service. By making the custom exception serializable, the microservice can transmit the entire exception object to the logging service, preserving all the crucial details. This facilitates detailed error analysis and helps identify the root cause of the issue.

Here’s an example of how you might log a serialized exception:

C try { // Code that might throw the custom exception } catch (CustomException ex) { // Serialize the exception IFormatter formatter = new BinaryFormatter(); Stream stream = new MemoryStream(); formatter.Serialize(stream, ex); string serializedException = Convert.ToBase64String(((MemoryStream)stream).ToArray()); // Log the serialized exception logger.Error(serializedException); }

This example uses a BinaryFormatter to serialize the exception into a memory stream, which is then converted to a base64 string for logging. Other serialization methods, such as JSON serialization, can also be used. The choice depends on the specific needs of your application.

Learn more about exception handling best practices.

External Resources:

[Infographic Placeholder: Visual representation of the serialization process for a custom exception]

Frequently Asked Questions

Q: What are the benefits of serializing custom exceptions?

A: Serializing exceptions allows you to preserve critical error information, especially in distributed environments. This facilitates debugging, logging, and error handling across different application tiers.

Q: When should I implement the ISerializable interface?

A: Implement ISerializable when you need granular control over which members of your exception are serialized or require custom serialization logic.

By understanding and implementing the techniques discussed in this article, you can ensure your .NET applications handle exceptions effectively, even in complex distributed environments. Correctly serializing exceptions preserves valuable debugging information, which is vital for diagnosing and resolving issues quickly. Start implementing these strategies today to enhance the robustness and maintainability of your applications. Explore further resources and delve deeper into advanced serialization techniques to build even more resilient error handling systems.

Question & Answer :
More specifically, when the exception contains custom objects which may or may not themselves be serializable.

Take this example:

public class MyException : Exception { private readonly string resourceName; private readonly IList<string> validationErrors; public MyException(string resourceName, IList<string> validationErrors) { this.resourceName = resourceName; this.validationErrors = validationErrors; } public string ResourceName { get { return this.resourceName; } } public IList<string> ValidationErrors { get { return this.validationErrors; } } } 

If this Exception is serialized and de-serialized, the two custom properties (ResourceName and ValidationErrors) will not be preserved. The properties will return null.

How can I implement serialization for custom exceptions?

Base implementation, without custom properties

SerializableExceptionWithoutCustomProperties.cs:

namespace SerializableExceptions { using System; using System.Runtime.Serialization; [Serializable] // Important: This attribute is NOT inherited from Exception, and MUST be specified // otherwise serialization will fail with a SerializationException stating that // "Type X in Assembly Y is not marked as serializable." public class SerializableExceptionWithoutCustomProperties : Exception { public SerializableExceptionWithoutCustomProperties() { } public SerializableExceptionWithoutCustomProperties(string message) : base(message) { } public SerializableExceptionWithoutCustomProperties(string message, Exception innerException) : base(message, innerException) { } // Without this constructor, deserialization will fail protected SerializableExceptionWithoutCustomProperties(SerializationInfo info, StreamingContext context) : base(info, context) { } } } 

Full implementation, with custom properties

Complete implementation of a custom serializable exception (MySerializableException), and a derived sealed exception (MyDerivedSerializableException).

The main points about this implementation are summarized here:

  1. You must decorate each derived class with the [Serializable] attribute — This attribute is not inherited from the base class, and if it is not specified, serialization will fail with a SerializationException stating that “Type X in Assembly Y is not marked as serializable.”
  2. You must implement custom serialization. The [Serializable] attribute alone is not enough — Exception implements ISerializable which means your derived classes must also implement custom serialization. This involves two steps:
    1. Provide a serialization constructor. This constructor should be private if your class is sealed, otherwise it should be protected to allow access to derived classes.
    2. Override GetObjectData() and make sure you call through to base.GetObjectData(info, context) at the end, in order to let the base class save its own state.

SerializableExceptionWithCustomProperties.cs:

namespace SerializableExceptions { using System; using System.Collections.Generic; using System.Runtime.Serialization; using System.Security.Permissions; [Serializable] // Important: This attribute is NOT inherited from Exception, and MUST be specified // otherwise serialization will fail with a SerializationException stating that // "Type X in Assembly Y is not marked as serializable." public class SerializableExceptionWithCustomProperties : Exception { private readonly string resourceName; private readonly IList<string> validationErrors; public SerializableExceptionWithCustomProperties() { } public SerializableExceptionWithCustomProperties(string message) : base(message) { } public SerializableExceptionWithCustomProperties(string message, Exception innerException) : base(message, innerException) { } public SerializableExceptionWithCustomProperties(string message, string resourceName, IList<string> validationErrors) : base(message) { this.resourceName = resourceName; this.validationErrors = validationErrors; } public SerializableExceptionWithCustomProperties(string message, string resourceName, IList<string> validationErrors, Exception innerException) : base(message, innerException) { this.resourceName = resourceName; this.validationErrors = validationErrors; } [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] // Constructor should be protected for unsealed classes, private for sealed classes. // (The Serializer invokes this constructor through reflection, so it can be private) protected SerializableExceptionWithCustomProperties(SerializationInfo info, StreamingContext context) : base(info, context) { this.resourceName = info.GetString("ResourceName"); this.validationErrors = (IList<string>)info.GetValue("ValidationErrors", typeof(IList<string>)); } public string ResourceName { get { return this.resourceName; } } public IList<string> ValidationErrors { get { return this.validationErrors; } } [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] public override void GetObjectData(SerializationInfo info, StreamingContext context) { if (info == null) { throw new ArgumentNullException("info"); } info.AddValue("ResourceName", this.ResourceName); // Note: if "List<T>" isn't serializable you may need to work out another // method of adding your list, this is just for show... info.AddValue("ValidationErrors", this.ValidationErrors, typeof(IList<string>)); // MUST call through to the base class to let it save its own state base.GetObjectData(info, context); } } } 

DerivedSerializableExceptionWithAdditionalCustomProperties.cs:

namespace SerializableExceptions { using System; using System.Collections.Generic; using System.Runtime.Serialization; using System.Security.Permissions; [Serializable] public sealed class DerivedSerializableExceptionWithAdditionalCustomProperty : SerializableExceptionWithCustomProperties { private readonly string username; public DerivedSerializableExceptionWithAdditionalCustomProperty() { } public DerivedSerializableExceptionWithAdditionalCustomProperty(string message) : base(message) { } public DerivedSerializableExceptionWithAdditionalCustomProperty(string message, Exception innerException) : base(message, innerException) { } public DerivedSerializableExceptionWithAdditionalCustomProperty(string message, string username, string resourceName, IList<string> validationErrors) : base(message, resourceName, validationErrors) { this.username = username; } public DerivedSerializableExceptionWithAdditionalCustomProperty(string message, string username, string resourceName, IList<string> validationErrors, Exception innerException) : base(message, resourceName, validationErrors, innerException) { this.username = username; } [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] // Serialization constructor is private, as this class is sealed private DerivedSerializableExceptionWithAdditionalCustomProperty(SerializationInfo info, StreamingContext context) : base(info, context) { this.username = info.GetString("Username"); } public string Username { get { return this.username; } } public override void GetObjectData(SerializationInfo info, StreamingContext context) { if (info == null) { throw new ArgumentNullException("info"); } info.AddValue("Username", this.username); base.GetObjectData(info, context); } } } 

Unit Tests

MSTest unit tests for the three exception types defined above.

UnitTests.cs:

namespace SerializableExceptions { using System; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using Microsoft.VisualStudio.TestTools.UnitTesting; [TestClass] public class UnitTests { private const string Message = "The widget has unavoidably blooped out."; private const string ResourceName = "Resource-A"; private const string ValidationError1 = "You forgot to set the whizz bang flag."; private const string ValidationError2 = "Wally cannot operate in zero gravity."; private readonly List<string> validationErrors = new List<string>(); private const string Username = "Barry"; public UnitTests() { validationErrors.Add(ValidationError1); validationErrors.Add(ValidationError2); } [TestMethod] public void TestSerializableExceptionWithoutCustomProperties() { Exception ex = new SerializableExceptionWithoutCustomProperties( "Message", new Exception("Inner exception.")); // Save the full ToString() value, including the exception message and stack trace. string exceptionToString = ex.ToString(); // Round-trip the exception: Serialize and de-serialize with a BinaryFormatter BinaryFormatter bf = new BinaryFormatter(); using (MemoryStream ms = new MemoryStream()) { // "Save" object state bf.Serialize(ms, ex); // Re-use the same stream for de-serialization ms.Seek(0, 0); // Replace the original exception with de-serialized one ex = (SerializableExceptionWithoutCustomProperties)bf.Deserialize(ms); } // Double-check that the exception message and stack trace (owned by the base Exception) are preserved Assert.AreEqual(exceptionToString, ex.ToString(), "ex.ToString()"); } [TestMethod] public void TestSerializableExceptionWithCustomProperties() { SerializableExceptionWithCustomProperties ex = new SerializableExceptionWithCustomProperties(Message, ResourceName, validationErrors); // Sanity check: Make sure custom properties are set before serialization Assert.AreEqual(Message, ex.Message, "Message"); Assert.AreEqual(ResourceName, ex.ResourceName, "ex.ResourceName"); Assert.AreEqual(2, ex.ValidationErrors.Count, "ex.ValidationErrors.Count"); Assert.AreEqual(ValidationError1, ex.ValidationErrors[0], "ex.ValidationErrors[0]"); Assert.AreEqual(ValidationError2, ex.ValidationErrors[1], "ex.ValidationErrors[1]"); // Save the full ToString() value, including the exception message and stack trace. string exceptionToString = ex.ToString(); // Round-trip the exception: Serialize and de-serialize with a BinaryFormatter BinaryFormatter bf = new BinaryFormatter(); using (MemoryStream ms = new MemoryStream()) { // "Save" object state bf.Serialize(ms, ex); // Re-use the same stream for de-serialization ms.Seek(0, 0); // Replace the original exception with de-serialized one ex = (SerializableExceptionWithCustomProperties)bf.Deserialize(ms); } // Make sure custom properties are preserved after serialization Assert.AreEqual(Message, ex.Message, "Message"); Assert.AreEqual(ResourceName, ex.ResourceName, "ex.ResourceName"); Assert.AreEqual(2, ex.ValidationErrors.Count, "ex.ValidationErrors.Count"); Assert.AreEqual(ValidationError1, ex.ValidationErrors[0], "ex.ValidationErrors[0]"); Assert.AreEqual(ValidationError2, ex.ValidationErrors[1], "ex.ValidationErrors[1]"); // Double-check that the exception message and stack trace (owned by the base Exception) are preserved Assert.AreEqual(exceptionToString, ex.ToString(), "ex.ToString()"); } [TestMethod] public void TestDerivedSerializableExceptionWithAdditionalCustomProperty() { DerivedSerializableExceptionWithAdditionalCustomProperty ex = new DerivedSerializableExceptionWithAdditionalCustomProperty(Message, Username, ResourceName, validationErrors); // Sanity check: Make sure custom properties are set before serialization Assert.AreEqual(Message, ex.Message, "Message"); Assert.AreEqual(ResourceName, ex.ResourceName, "ex.ResourceName"); Assert.AreEqual(2, ex.ValidationErrors.Count, "ex.ValidationErrors.Count"); Assert.AreEqual(ValidationError1, ex.ValidationErrors[0], "ex.ValidationErrors[0]"); Assert.AreEqual(ValidationError2, ex.ValidationErrors[1], "ex.ValidationErrors[1]"); Assert.AreEqual(Username, ex.Username); // Save the full ToString() value, including the exception message and stack trace. string exceptionToString = ex.ToString(); // Round-trip the exception: Serialize and de-serialize with a BinaryFormatter BinaryFormatter bf = new BinaryFormatter(); using (MemoryStream ms = new MemoryStream()) { // "Save" object state bf.Serialize(ms, ex); // Re-use the same stream for de-serialization ms.Seek(0, 0); // Replace the original exception with de-serialized one ex = (DerivedSerializableExceptionWithAdditionalCustomProperty)bf.Deserialize(ms); } // Make sure custom properties are preserved after serialization Assert.AreEqual(Message, ex.Message, "Message"); Assert.AreEqual(ResourceName, ex.ResourceName, "ex.ResourceName"); Assert.AreEqual(2, ex.ValidationErrors.Count, "ex.ValidationErrors.Count"); Assert.AreEqual(ValidationError1, ex.ValidationErrors[0], "ex.ValidationErrors[0]"); Assert.AreEqual(ValidationError2, ex.ValidationErrors[1], "ex.ValidationErrors[1]"); Assert.AreEqual(Username, ex.Username); // Double-check that the exception message and stack trace (owned by the base Exception) are preserved Assert.AreEqual(exceptionToString, ex.ToString(), "ex.ToString()"); } } }