Clean&Reactoring Clean Code Created: 09 Feb 2026 Updated: 09 Feb 2026

Custom Exceptions in C#: When and How to Use Them

In C#, the standard library provides a rich set of exceptions like ArgumentNullException, InvalidOperationException, and FileNotFoundException. However, sometimes your application encounters errors that are specific to your business logic—errors that standard exceptions simply cannot describe accurately.

This is where Custom Exceptions come in. They allow you to create specific, meaningful error types that make your code easier to debug and maintain.

1. How to Create a Custom Exception

To create a custom exception, you simply create a class that inherits from the base Exception class.

Best Practice: Always implement the three standard constructors so your exception behaves like any other .NET exception.

using System;

namespace MyApplication.Exceptions
{
// 1. Inherit from 'Exception'
// 2. End the class name with 'Exception'
public class InsufficientFundsException : Exception
{
// Property to hold extra data useful for debugging or UI display
public decimal CurrentBalance { get; }
public decimal AttemptedAmount { get; }

// Standard Constructor 1: Default
public InsufficientFundsException()
{
}

// Standard Constructor 2: Message only
public InsufficientFundsException(string message)
: base(message)
{
}

// Standard Constructor 3: Message + Inner Exception (for wrapping)
public InsufficientFundsException(string message, Exception inner)
: base(message, inner)
{
}

// Custom Constructor: specialized for your specific error data
public InsufficientFundsException(decimal currentBalance, decimal attemptedAmount)
: base($"Transaction failed. Available: {currentBalance}, Required: {attemptedAmount}")
{
CurrentBalance = currentBalance;
AttemptedAmount = attemptedAmount;
}
}
}

2. When to Use Custom Exceptions

You should not create a custom exception for every possible error. Overusing them adds unnecessary complexity. Use them only when:

  1. You need to handle this specific error differently: If you want to catch only this error and let others bubble up, a custom type is essential.
  2. You need to carry additional data: Standard exceptions only allow a string message. Custom exceptions can carry properties (like ErrorCode, UserId, or RetryAfter).
  3. No standard exception fits: If InvalidOperationException feels too vague, create a specific one like OrderAlreadyShippedException.

Decision Logic:

  1. Is the argument null? -> Use ArgumentNullException.
  2. Is the file missing? -> Use FileNotFoundException.
  3. Did the user try to withdraw more money than they have? -> Create InsufficientFundsException.

3. How to Throw and Catch

Here is how you use the custom exception we defined above. Notice how much cleaner the logic becomes.

Throwing the Exception

public class BankAccount
{
public decimal Balance { get; private set; }

public void Withdraw(decimal amount)
{
if (amount > Balance)
{
// We throw our specific exception with data attached
throw new InsufficientFundsException(Balance, amount);
}
Balance -= amount;
}
}

Catching the Exception

When catching, we can now target this specific error type. This allows us to show a helpful message to the user, while letting "real" crashes (like NullReferenceException) bubble up to be logged.

try
{
myAccount.Withdraw(500);
}
catch (InsufficientFundsException ex)
{
// We can access the custom properties to show a helpful UI
Console.WriteLine($"Error: {ex.Message}");
Console.WriteLine($"You are short by: {ex.AttemptedAmount - ex.CurrentBalance}");
}
catch (Exception ex)
{
// Generic fallback for unexpected errors
Console.WriteLine("An unexpected error occurred.");
}

Summary

FeatureStandard ExceptionCustom Exception
PurposeGeneral errors (Nulls, IO, Math)Domain-specific errors (Business Logic)
DataMessage (String)Any Properties (Int, Objects, Enums)
HandlingGeneric catch blocksSpecific catch blocks
ExampleArgumentExceptionInvalidPaymentException


Share this lesson: