Creating Custom Exceptions in C#

Vaibhav • September 10, 2025

In the previous articles, we explored the fundamentals of exception handling in C# - including try, catch, finally, and built-in exception types. These tools help us gracefully handle unexpected situations. But what happens when the built-in exceptions don’t quite fit your scenario? That’s where custom exceptions come in. In this article, we’ll learn how to define our own exception types, when and why to use them, and how to integrate them into real-world codebases.

Why Create Custom Exceptions?

Built-in exceptions like ArgumentNullException or InvalidOperationException cover many common cases. But sometimes, your application has domain-specific rules that need clearer signaling. For example, in a banking app, you might want to throw a InsufficientFundsException instead of a generic Exception. This improves readability, debugging, and error handling.

Custom exceptions make your code more expressive. They communicate intent better than generic exceptions and allow consumers of your API to handle specific failure cases.

How to Define a Custom Exception

Creating a custom exception is straightforward. You define a new class that inherits from System.Exception (or a more specific base like ApplicationException), and provide constructors that mirror the base class.

public class InsufficientFundsException : Exception
{
    public InsufficientFundsException() 
    {
    }

    public InsufficientFundsException(string message) 
        : base(message)
    {
    }

    public InsufficientFundsException(string message, Exception inner) 
        : base(message, inner)
    {
    }
}

This class defines three constructors:

  • A parameterless constructor for default messages.
  • A constructor that accepts a custom message.
  • A constructor that accepts a message and an inner exception (for exception chaining).

Always provide the three standard constructors so your exception integrates well with .NET’s exception handling infrastructure.

Throwing a Custom Exception

Once defined, you can throw your custom exception just like any other:

decimal balance = 100m;
decimal withdrawal = 150m;

if (withdrawal > balance)
{
    throw new InsufficientFundsException("Withdrawal exceeds available balance.");
}

This code checks a business rule and throws a domain-specific exception when violated. The message helps developers and users understand what went wrong.

Catching Custom Exceptions

You can catch custom exceptions using a catch block that targets the specific type:

try
{
    ProcessTransaction();
}
catch (InsufficientFundsException ex)
{
    Console.WriteLine("Transaction failed: " + ex.Message);
}

This allows you to handle different exception types differently. For example, you might retry on a network error but show a warning for insufficient funds.

Best Practices for Custom Exceptions

Custom exceptions are powerful, but they should be used judiciously. Here are some guidelines:

  • Use meaningful names: The name should clearly describe the error condition.
  • Inherit from Exception: Avoid inheriting from SystemException or other low-level types.
  • Include context: Use the message to explain what went wrong and why.
  • Support serialization: If your exception might cross application boundaries, implement ISerializable.

Don’t create custom exceptions for every error. Use them when they represent a distinct, actionable condition in your domain.

Custom Exception with Additional Properties

Sometimes you want to include extra data in your exception. You can add properties to your custom class:

public class ValidationException : Exception
{
    public string FieldName { get; }

    public ValidationException(string fieldName, string message)
        : base(message)
    {
        FieldName = fieldName;
    }
}

This lets you pass structured information to the caller. For example:

throw new ValidationException("Email", "Email address is invalid.");

The caller can inspect FieldName to highlight the problematic field in the UI.

Exception Chaining and Inner Exceptions

When wrapping one exception inside another, use the inner constructor:

try
{
    SaveToDatabase();
}
catch (SqlException ex)
{
    throw new DataAccessException("Failed to save record.", ex);
}

This preserves the original exception for debugging while providing a higher-level context.

The InnerException property is automatically populated when you use the constructor that accepts an exception. This helps trace the full error stack.

Custom Exceptions in Larger Applications

In real-world projects, you might organize exceptions into namespaces or folders. For example:

namespace MyApp.Exceptions
{
    public class UserNotFoundException : Exception { ... }
    public class PermissionDeniedException : Exception { ... }
}

This keeps your codebase clean and makes it easier to find and reuse exception types.

Logging and Custom Exceptions

When logging exceptions, include the type name and message. For example:

catch (Exception ex)
{
    logger.LogError($"Exception: {ex.GetType().Name}, Message: {ex.Message}");
}

This helps you distinguish between different failure modes in logs and dashboards.

Common Mistakes to Avoid

  • Using generic Exception: Avoid throwing new Exception("Something went wrong"). It’s vague and unhelpful.
  • Not providing a message: Always include a descriptive message to aid debugging.
  • Overusing custom exceptions: Don’t create a new type for every minor error. Use them for meaningful, domain-specific conditions.
  • Ignoring inner exceptions: Always preserve the original exception when wrapping.

Summary

Custom exceptions are a powerful way to express domain-specific errors in C#. They improve clarity, enable targeted error handling, and make your codebase easier to maintain. By inheriting from Exception, providing standard constructors, and optionally adding context properties, you can create robust exception types that integrate seamlessly with .NET’s error handling model. Use them thoughtfully, and your application will be easier to debug, test, and extend.

In the next article, we’ll explore Exception Propagation - how exceptions travel through the call stack, and how to design your methods to handle or rethrow them appropriately.