7 Techniques to Master Error Handling in C#
Guide for better Error Handling: principles, recommendations, and techniques on how to work with exceptions in C#.
Error handling is an essential part of a programmer’s life. Inputs can be abnormal, and applications can fail. We need to make sure that our code does what it’s supposed to do.
In this article, I will outline the techniques and recommendations that you can use to write code that handles errors and stays clean. For code examples, I will use C# 8.0.
Don’t Return and Pass Null
The best approach to handling an exception is to avoid making them. Nothing good comes from returning or passing null
. NullReferenceException
is the most thrown exception in C#, and we shouldn’t support it anymore. Naturally, if you are in need to return or pass the default value, then pass the default value.
- If you need to return the default value of a
string
— just returnstring.Empty
. - If you need to return the default value of
List
— justreturn Enumerable.Empty<T>()
. - If you need to return the default value of your
class
— just create the default representation of it.
public class Order{
public int Id {get;set;}
public string OrderNumber {get;set;}
public static Order EMPTY = new Order{OrderNumber = string.Empty}
}
Apply the same rules for passing, and your life will be less painful.
Extract Try Blocks
When the sequence of commands in the try block becomes longer and less readable. Don’t be afraid to extract such blocks into new functions.
public class OrderRepository{
public void DeleteOrder(int id){
try{
context.Orders.Remove(id);
var ordersItems = context.OrderItems.Where(oi => oi.OrderId == id);
context.OrderItems.RemoveRange(ordersItems);
context.SaveChanges();
}
catch(Exception ex){
//handle exception
}
}
}
public class OrderRepository{
public bool TryDeleteOrder(int id){
try{
DeleteOrder(id);
return true;
}
catch(Exception ex){
//handle exception
return false;
}
}
public void DeleteOrder(int id){
context.Orders.Remove(id);
var ordersItems = context.OrderItems.Where(oi => oi.OrderId == id);
context.OrderItems.RemoveRange(ordersItems);
context.SaveChanges();
}
}
Throw an Exception Instead of Return Codes
Returning error codes was a common technique in programming languages in the near past. Today, the exceptions approach penetrated into almost every programming language in such a way that returning codes is an almost death technique.
The following code sample is an illustration of returning codes that you should avoid.
public class HomeController{
private readonly IHomeService homeService;
public HomeController(IHomeService homeService){
this.homeService = homeService;
}
[HttpPost]
public Task<IActionResult> Clean(){
CleanStatus cleanStatus = homeService.Clean();
if(cleanStatus != CleanStatus.INVALID){
//handle invalid state
}
}
}
There are two main problems with this approach;
- The Caller
Clean()
gets messier. Its logic is obscured by error handling. TheTry-catch
block is much more readable thanif
statement when it comes to error handling. Especially in cases of handling multiple error types. - The return value has to be of an error code type. This can be easily overcome by creating the class that will represent the return type. But we should not make the code more complex by avoiding using
try-catch
block.
public class HomeController{
private readonly IHomeService homeService;
public HomeController(IHomeService homeService){
this.homeService = homeService;
}
[HttpPost]
public Task<IActionResult> Clean(){
try{
homeService.Clean();
}catch(InvalidStatusException ex){
//handle invalid state
}
}
}
Use C# Exception Filter Feature
Programming language C# is blessed by syntax sugar keyword when. Catch block can continue after the end of the parenthesis and work with the Exception
context.
catch (InvalidCastException e)
{
if (e.Data == null)
{
throw;
}
else
{
// Take some action.
}
}
catch (InvalidCastException e) when (e.Data != null)
{
// Take some action.
}
Write your Try-Catch-Finally Statement First
The interesting thing about exceptions is that they define a scope within your program. Once you use the try-catch
statement, you are deciding that exception can interrupt execution at any point and then resume at the catch
.
The try-catch
blocks are like transactions. The catch
has to leave your program in a consistent state, no matter what happens in the try
. It is a good practice to start with a try-catch-finally
statement when you are writing code that could throw an exception.
This practice helps you to define what the user should expect and design proper actions based on it, like printing a user-friendly error message or killing an application.
public class TicketManager
{
public void PrintTicket(Ticket ticket){
try{
printingService.OpenConnection();
printingService.Print(ticket);
}
catch(PrintingMachineNotFoundException ex){
ShowDialog("Printer cannot be found. Connect printer and try again later.")
}
catch(PaperTrayEmptyException ex){
ShowDialog("Printer is out of paper.")
}
finally{
printingService.CloseConnection();
}
}
}
Provide Context with Exceptions
Each exception that you throw should provide enough context to figure out the source and location of an error. In C#, you can get a StackTrace
from any exception; however, trace can’t tell you the intent of the operation that failed and also can be unreadable when you are using await/async
.
Create an informative message and pass them along with your exceptions.
Wrap an External API and Handle Exception in Wrapper Only
Nowadays, it is not programming about developing everything by yourself but developing communication with everything. Every little tool for anything you can imagine has its public API, which throws an exception.
Also, we live in a time where supply is higher than the offer. It is widespread that you will change the third-party tool once. The new API can throw a different type of exception, and we can be more prepared for such a situation.
public class UserProfileController{
[HttpGet("{id}")]
public IActionResult GetUsersTimeEntries(int id){
try{
var timeEntryService = new TimeEntryService(userId);
return timeEntryService.Get();
}
catch(ExternalApiTypeException ex){
//handle exception
}
catch(ExternalApiTypeException2 ex){
//handle exception
}
catch(ExternalApiTypeException3 ex){
//handle exception
}
}
}
TimeEntryService
represents an external service that implements the HTTP communication with Rest API of time tracking application. Tracking application might throw three different types of an exception.
Once we wrapped the communication with UserTimeEntriesService
, we are achieving particular dynamics. When we switch our tracking application with service, we will simply create a new implementation of an IUserTimeEntriesService
and its method Get
.
public interface IUserTimeEntriesService{
IEnumerable<TimeEntry> Get(int userId);
}
public class UserTimeEntriesService{
public IEnumerable<TimeEntry> Get(int userId){
try{
return TryGet(userId);
}
catch(ExternalApiTypeException ex){
//handle exception
}
catch(ExternalApiTypeException2 ex){
//handle exception
}
catch(ExternalApiTypeException3 ex){
//handle exception
}
}
private IEnumerable<TimeEntry> TryGet(int userId){
var timeEntryService = new TimeEntryService(userId);
return timeEntryService.Get();
}
}
public class UserProfileController{
private readonly IUserTimeEntriesService userTimeEntriesService;
public UserProfileController(IUserTimeEntriesService userTimeEntriesService){
this.userTimeEntriesService = userTimeEntriesService;
}
[HttpGet("{id}")]
public IActionResult GetUsersTimeEntries(int id){
var results = userTimeEntriesService.Get(id);
return Ok(results);
}
}
Summary
Clean Code and Error Handling are not the best buddies. Clean Code wants to be readable as beautiful prose, and Error Handling is like bad verses to it. But we could find compromises to satisfy both camps.
The primary source of the blog post is the book Clean Code: A Handbook of Agile Software Craftsmanship.