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

Open/Closed Principle (OCP) in C#: Flexible Exception Handling

The Open/Closed Principle (OCP) is the "O" in SOLID. It states:

"Software entities (classes, modules, functions) should be open for extension, but closed for modification."

In simple terms, you should be able to add new features (extensions) without rewriting existing, tested code (modification).

When applied to Exception Handling, this principle transforms a rigid try-catch block into a dynamic, plugin-style system. Let's explore how to achieve this using the Strategy Pattern.

1. The Problem: The "Closed" Approach

In a traditional (and often problematic) design, the FileProcessor knows exactly how to handle every specific error.

public class FileProcessor
{
public void ProcessFile(string path)
{
try
{
// Reading logic...
}
catch (FileNotFoundException ex)
{
// Logic A: Log missing file
}
catch (IOException ex)
{
// Logic B: Log IO error
}
catch (UnauthorizedAccessException ex)
{
// Logic C: Log permission error
}
}
}

Why is this bad?

  1. Violation of OCP: Every time you want to handle a new exception (e.g., JsonException), you must modify the FileProcessor class.
  2. High Complexity: As the list of exceptions grows, this method becomes a massive, unreadable block of logic.

2. The Solution: The "Open" Approach

To follow OCP, we want to extend the exception handling logic without touching the FileProcessor. We do this by defining a contract (Interface) for handling errors.

Step 1: The Abstraction

First, we define an interface that represents "The ability to handle an exception."

public interface IExceptionHandler
{
// Returns true if the exception was handled, false otherwise.
bool HandleException(Exception ex);
}

Step 2: Concrete Strategies

Now, we create small, focused classes for each specific error type. These classes are Extensions.

Handler for Missing Files:

public class FileNotFoundExceptionHandler : IExceptionHandler
{
private readonly ILogger _logger;

public FileNotFoundExceptionHandler(ILogger logger)
{
_logger = logger;
}

public bool HandleException(Exception ex)
{
// This handler only cares about FileNotFoundException
if (ex is FileNotFoundException fnfEx)
{
_logger.LogError($"File not found: {fnfEx.FileName}");
return true; // Handled!
}
return false; // Not my responsibility
}
}

Handler for IO Errors:

public class IOExceptionHandler : IExceptionHandler
{
private readonly ILogger _logger;

public IOExceptionHandler(ILogger logger)
{
_logger = logger;
}

public bool HandleException(Exception ex)
{
if (ex is IOException)
{
_logger.LogError($"Error reading file: {ex.Message}");
return true;
}
return false;
}
}

Handler for Everything Else (Fallback):

public class UnexpectedExceptionHandler : IExceptionHandler
{
private readonly ILogger _logger;

public UnexpectedExceptionHandler(ILogger logger)
{
_logger = logger;
}

public bool HandleException(Exception ex)
{
_logger.LogError($"Unexpected error: {ex.Message}");
return true;
}
}

Step 3: The Context (The File Processor)

Finally, we update the FileProcessor. It no longer hard-codes catch blocks. Instead, it asks a list of handlers to do the work.

public class FileProcessor
{
private readonly IEnumerable<IExceptionHandler> _exceptionHandlers;

// Dependency Injection: We inject a list of ALL available handlers
public FileProcessor(IEnumerable<IExceptionHandler> exceptionHandlers)
{
_exceptionHandlers = exceptionHandlers;
}

public void ProcessFile(string filePath)
{
try
{
// Code that reads and processes the file...
System.IO.File.ReadAllText(filePath);
}
catch (Exception ex)
{
HandleError(ex);
}
}

private void HandleError(Exception ex)
{
bool handled = false;

// Iterate through extensions to find one that can handle this error
foreach (var handler in _exceptionHandlers)
{
if (handler.HandleException(ex))
{
handled = true;
break; // Stop once handled
}
}

// If no handler could fix it, re-throw the exception
if (!handled)
{
throw ex;
}
}
}

Why is this OCP Compliant?

Imagine a new requirement comes in: "We need to handle OutOfMemoryException specifically by sending an admin email."

  1. Old Way: You would open FileProcessor.cs and add a catch (OutOfMemoryException) block. (Modification)
  2. OCP Way: You create a new class OutOfMemoryHandler : IExceptionHandler and register it in your dependency injection container. You do not touch FileProcessor.cs. (Extension)

By separating the "What" (the processing) from the "How" (the error handling), your code becomes cleaner, testable, and robust against future changes.

Share this lesson: