Skip to main content

C# Custom Exceptions

Introduction

When developing C# applications, you'll often encounter situations where the built-in exception types don't adequately describe the specific error that has occurred in your application. This is where custom exceptions come into play. Custom exceptions allow you to define your own exception types tailored to your application's specific requirements.

In this tutorial, you'll learn:

  • Why and when to create custom exceptions
  • How to implement custom exception classes
  • Best practices for using custom exceptions
  • Real-world examples of custom exceptions

Why Create Custom Exceptions?

The .NET Framework provides many built-in exception types, but sometimes you need to create your own exceptions to:

  1. Provide clearer error information specific to your application domain
  2. Distinguish between different error conditions that might otherwise use the same standard exception
  3. Add additional properties to capture contextual information about the error
  4. Improve exception handling with more specific catch blocks

Creating a Basic Custom Exception

Creating a custom exception in C# is straightforward. Your custom exception class should:

  1. Derive from Exception or a more specific exception class
  2. Follow the naming convention of ending with "Exception"
  3. Implement the standard constructors
  4. Be marked as [Serializable] for proper serialization support

Here's a simple example of a custom exception:

csharp
using System;
using System.Runtime.Serialization;

[Serializable]
public class UserNotFoundException : Exception
{
// Default constructor
public UserNotFoundException() : base() { }

// Constructor with message
public UserNotFoundException(string message) : base(message) { }

// Constructor with message and inner exception
public UserNotFoundException(string message, Exception innerException)
: base(message, innerException) { }

// Constructor for serialization
protected UserNotFoundException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
}

Using Custom Exceptions

Once you've defined your custom exception, you can throw it just like any standard exception:

csharp
public User GetUserById(int userId)
{
User user = _userRepository.FindById(userId);

if (user == null)
{
throw new UserNotFoundException($"User with ID {userId} was not found.");
}

return user;
}

And you can catch it specifically in your exception handling code:

csharp
try
{
User user = userService.GetUserById(123);
Console.WriteLine($"Found user: {user.Name}");
}
catch (UserNotFoundException ex)
{
Console.WriteLine($"User error: {ex.Message}");
// Handle specifically when a user is not found
}
catch (Exception ex)
{
Console.WriteLine($"General error: {ex.Message}");
// Handle other exceptions
}

Adding Custom Properties

One of the advantages of custom exceptions is the ability to include additional properties that provide more context about the error:

csharp
[Serializable]
public class OrderProcessingException : Exception
{
public int OrderId { get; }
public string OrderStatus { get; }

public OrderProcessingException(string message, int orderId, string orderStatus)
: base(message)
{
OrderId = orderId;
OrderStatus = orderStatus;
}

public OrderProcessingException(string message, int orderId, string orderStatus, Exception innerException)
: base(message, innerException)
{
OrderId = orderId;
OrderStatus = orderStatus;
}

// Other constructors...

// Required for serialization
protected OrderProcessingException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
OrderId = info.GetInt32("OrderId");
OrderStatus = info.GetString("OrderStatus");
}

// Override GetObjectData for serialization
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue("OrderId", OrderId);
info.AddValue("OrderStatus", OrderStatus);
}
}

Real-World Example: Banking Application

Let's see how custom exceptions can improve a banking application's error handling:

csharp
[Serializable]
public class InsufficientFundsException : Exception
{
public decimal RequestedAmount { get; }
public decimal AvailableBalance { get; }

public InsufficientFundsException(decimal requestedAmount, decimal availableBalance)
: base($"Cannot withdraw ${requestedAmount}. Available balance is ${availableBalance}.")
{
RequestedAmount = requestedAmount;
AvailableBalance = availableBalance;
}

// Include other constructors and serialization support...
}

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

public BankAccount(string accountNumber, decimal initialBalance)
{
AccountNumber = accountNumber;
Balance = initialBalance;
}

public void Withdraw(decimal amount)
{
if (amount <= 0)
{
throw new ArgumentException("Withdrawal amount must be positive", nameof(amount));
}

if (amount > Balance)
{
throw new InsufficientFundsException(amount, Balance);
}

Balance -= amount;
Console.WriteLine($"Withdrew ${amount}. New balance: ${Balance}");
}
}

Using the custom exception:

csharp
try
{
BankAccount account = new BankAccount("12345", 100);
account.Withdraw(150); // Trying to withdraw more than the balance
}
catch (InsufficientFundsException ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine($"You need ${ex.RequestedAmount - ex.AvailableBalance} more to complete this transaction.");
Console.WriteLine("Would you like to make a deposit first?");
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
}

Output:

Cannot withdraw $150. Available balance is $100.
You need $50 more to complete this transaction.
Would you like to make a deposit first?

Creating an Exception Hierarchy

For complex applications, it makes sense to create a hierarchy of custom exceptions:

csharp
// Base exception for all application-specific exceptions
[Serializable]
public class AppException : Exception
{
public AppException() : base() { }
public AppException(string message) : base(message) { }
public AppException(string message, Exception innerException) : base(message, innerException) { }
protected AppException(SerializationInfo info, StreamingContext context) : base(info, context) { }
}

// Domain-specific exceptions
[Serializable]
public class UserException : AppException
{
public UserException(string message) : base(message) { }
// Other constructors...
}

[Serializable]
public class PaymentException : AppException
{
public PaymentException(string message) : base(message) { }
// Other constructors...
}

// Specific exceptions
[Serializable]
public class InvalidUserCredentialsException : UserException
{
public InvalidUserCredentialsException() : base("Invalid username or password.") { }
// Other constructors...
}

[Serializable]
public class PaymentDeclinedException : PaymentException
{
public string DeclineReason { get; }

public PaymentDeclinedException(string declineReason)
: base($"Payment was declined: {declineReason}")
{
DeclineReason = declineReason;
}
// Other constructors...
}

Best Practices for Custom Exceptions

  1. Follow naming conventions: Name your exception classes with the "Exception" suffix.

  2. Derive from the appropriate base class: Inherit from Exception or a more specific exception type that best fits the error condition.

  3. Make exceptions serializable: Mark your exception classes with the [Serializable] attribute and implement the serialization constructor.

  4. Include standard constructors: Implement the standard constructor patterns found in the base Exception class.

  5. Provide meaningful error messages: Make your exception messages clear and descriptive.

  6. Document your exceptions: Use XML comments to document when and why your exceptions might be thrown.

  7. Don't overuse custom exceptions: Create custom exceptions only when they add value by distinguishing important error cases.

  8. Keep exceptions for exceptional conditions: Don't use exceptions for normal flow control.

When to Use Standard vs. Custom Exceptions

Use standard exceptions when:

  • The existing exception type accurately describes the error
  • No additional context or properties are needed
  • The error is common and well understood

Use custom exceptions when:

  • The error is specific to your application domain
  • You need to add extra properties to provide context
  • You want to enable specific exception handling
  • The standard exceptions don't adequately describe the error condition

Summary

Custom exceptions in C# allow you to create more specific error types for your application, making your code more readable and your error handling more precise. By creating well-designed custom exception classes, you can:

  • Provide clearer error messages to users
  • Add additional context through custom properties
  • Enable more specific exception handling
  • Create a domain-specific exception hierarchy

Remember to follow the best practices when creating custom exceptions, such as making them serializable, following naming conventions, and implementing the standard constructors.

Exercises

  1. Create a custom exception called ConfigurationMissingException that includes a property for the missing configuration key.

  2. Create an exception hierarchy for a library management system with at least three specific exception types.

  3. Modify an existing application to replace generic exceptions with custom exceptions that provide more context.

  4. Create a custom exception that logs details to a file when it is created.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)