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 fromSystemException
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 throwingnew 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.