.NET
Exception Handling
Serialization
Custom Exceptions
Programming

Make a custom .NET Exception serializable

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

A custom .NET exception is only fully aligned with the platform conventions when it follows the standard exception pattern, including serialization support where required. The key pieces are the [Serializable] attribute, the normal exception constructors, and a serialization constructor that restores custom state. If your exception adds extra properties, you also need to write them into SerializationInfo.

The Standard Exception Pattern

At minimum, a custom exception should derive from Exception and provide the usual constructors:

csharp
1using System;
2using System.Runtime.Serialization;
3
4[Serializable]
5public class ValidationFailedException : Exception
6{
7    public ValidationFailedException()
8    {
9    }
10
11    public ValidationFailedException(string message)
12        : base(message)
13    {
14    }
15
16    public ValidationFailedException(string message, Exception innerException)
17        : base(message, innerException)
18    {
19    }
20
21    protected ValidationFailedException(SerializationInfo info, StreamingContext context)
22        : base(info, context)
23    {
24    }
25}

This already makes the exception participate correctly in the standard serialization pattern expected by classic .NET code.

Adding Custom Properties

The real reason serialization support matters is when the exception carries extra data. Suppose the exception includes a field name and an error code:

csharp
1using System;
2using System.Runtime.Serialization;
3
4[Serializable]
5public class ValidationFailedException : Exception
6{
7    public string FieldName { get; }
8    public int ErrorCode { get; }
9
10    public ValidationFailedException()
11    {
12    }
13
14    public ValidationFailedException(string message)
15        : base(message)
16    {
17    }
18
19    public ValidationFailedException(string message, Exception innerException)
20        : base(message, innerException)
21    {
22    }
23
24    public ValidationFailedException(string message, string fieldName, int errorCode)
25        : base(message)
26    {
27        FieldName = fieldName;
28        ErrorCode = errorCode;
29    }
30
31    protected ValidationFailedException(SerializationInfo info, StreamingContext context)
32        : base(info, context)
33    {
34        FieldName = info.GetString(nameof(FieldName)) ?? "";
35        ErrorCode = info.GetInt32(nameof(ErrorCode));
36    }
37
38    public override void GetObjectData(SerializationInfo info, StreamingContext context)
39    {
40        if (info == null)
41        {
42            throw new ArgumentNullException(nameof(info));
43        }
44
45        info.AddValue(nameof(FieldName), FieldName);
46        info.AddValue(nameof(ErrorCode), ErrorCode);
47        base.GetObjectData(info, context);
48    }
49}

Without the overridden GetObjectData, the extra properties would be lost during serialization and deserialization.

Why the Serialization Constructor Matters

The protected constructor that accepts SerializationInfo and StreamingContext is how .NET reconstructs the exception. It should restore the custom data that you previously stored in GetObjectData.

If you omit this constructor on a custom exception with extra properties, deserialization may fail or your custom state may come back empty. That is the main reason the full pattern exists.

When You Actually Need This

In many modern application codebases, custom exceptions are never serialized manually. So this pattern is most relevant when:

  • a framework or library expects serializable exception types
  • the exception crosses process or application boundaries
  • tooling captures and reconstructs exception data
  • you want a platform-conventional exception type for reusable libraries

If the exception stays entirely inside one process and carries no extra state, serialization support may never be exercised in practice. Even so, many teams still implement the standard pattern for completeness and compatibility.

Testing the Custom State

At a minimum, test that the custom properties survive construction and, if relevant to your environment, serialization flows. Even without a full serialization test, verifying the property values and constructor chaining catches most implementation mistakes.

csharp
1using Xunit;
2
3public class ValidationFailedExceptionTests
4{
5    [Fact]
6    public void Stores_Custom_State()
7    {
8        var ex = new ValidationFailedException("bad input", "Email", 1001);
9
10        Assert.Equal("Email", ex.FieldName);
11        Assert.Equal(1001, ex.ErrorCode);
12    }
13}

The more data your exception carries, the more valuable these checks become.

Common Pitfalls

The most common pitfall is adding custom properties and forgetting to include them in GetObjectData and the serialization constructor.

Another pitfall is omitting one or more standard constructors. That makes the exception less compatible with common usage patterns, especially when wrapping inner exceptions.

Developers also forget to call base.GetObjectData(info, context). If you skip that call, base exception state may not be preserved correctly.

Finally, do not add a large amount of mutable business state to exceptions. Exceptions should explain failures, not become alternative transport objects.

Summary

  • Mark the custom exception with [Serializable].
  • Provide the standard exception constructors, including the protected serialization constructor.
  • Override GetObjectData when the exception adds custom properties.
  • Restore those custom properties from SerializationInfo in the serialization constructor.
  • Keep exception payloads focused on error context rather than broad application state.

Course illustration
Course illustration

All Rights Reserved.