skip to content

⚠️ Clear And Actionable Error Messages In .Net

Achieve robust error handling in .Net APIs by implementing best practices with the Generic class and lambda expressions

Introduction

Handling errors in web APIs can be a tedious task. But with the right approach, it can be made simple and effective. In this blog post, we’ll discuss the importance of providing clear and specific error messages in web APIs and the best practices for achieving that. We’ll take a close look at the generic exception class and how it can be used to handle errors in a consistent and clear way.

Problem 🤔

The problem we are trying to address in this blog post is the lack of consistency and specificity in error handling in web APIs. Traditional error handling methods often lead to generic and unhelpful error messages that do not provide enough information for the user or developer to understand and resolve the issue. This can lead to frustration for the user and can also make it difficult for developers to identify and fix the problem.

Lets take the CreateAccount action method. The method has a simple validation logic and returns a BadRequest result if the person object does not pass the validation. Error should allow the client to receive clear and specific error messages that can be used to identify and resolve the issue.

[HttpPost]
public IActionResult CreateAccount([FromBody] Person person)
{
  // 🔬 Validation Logic
 
  // ⚠️ Return Error If Person Is Invalid
 
  // 👌 Continue...
}
[HttpPost]
public IActionResult CreateAccount([FromBody] Person person)
{
  // 🔬 Validation Logic
 
  // ⚠️ Return Error If Person Is Invalid
 
  // 👌 Continue...
}

Person can be a simple class or record with three properties Name, Address and Mobile.

public class Person
{
    public string Name { get; set; }
    public string Address { get; set; }
    public string Mobile { get; set; }
};
public class Person
{
    public string Name { get; set; }
    public string Address { get; set; }
    public string Mobile { get; set; }
};
public record Person(string Name, string Address, string Mobile);
public record Person(string Name, string Address, string Mobile);

Solution/TLDR 🎯

We can create generic class InvalidInput<TSource> that can be used to return errors for any type. The Add method takes a lambda expression that selects a property of the source type, and a string message.

InvalidInput

This code defines a generic InvalidInput<TSource> class that can be used to store a collection of invalid properties and their associated error messages, it has a dictionary to map error messages with respective properties, and Add method to add properties to the invalid properties dictionary and uses lambda expression to get the name of the property.

public class InvalidInput<TSource>
{
  // Map error messages with respective property
  public Dictionary<string, string> InvalidProperties { get; } = new();
 
  // Add method for adding properties to the invalid properties dictionary
  public InvalidInput<TSource> Add<TProp>(
    Expression<Func<TSource, TProp>> keySelector,
    string message
  )
  {
    // Use the lambda expression to get the name of the property
    var propertyName = ((MemberExpression)keySelector.Body).Member.Name;
    InvalidProperties[propertyName] = message;
    return this;
  }
}
public class InvalidInput<TSource>
{
  // Map error messages with respective property
  public Dictionary<string, string> InvalidProperties { get; } = new();
 
  // Add method for adding properties to the invalid properties dictionary
  public InvalidInput<TSource> Add<TProp>(
    Expression<Func<TSource, TProp>> keySelector,
    string message
  )
  {
    // Use the lambda expression to get the name of the property
    var propertyName = ((MemberExpression)keySelector.Body).Member.Name;
    InvalidProperties[propertyName] = message;
    return this;
  }
}

Usage

The CreateAccount method creates an instance of the InvalidInput<Person> class. In this case, the lambda expressions are used to select the Address and Mobile properties of the Person class, and assigning the error message to it.

[HttpPost]
public IActionResult CreateAccount([FromBody] Person person)
{
  // 🏗️ Build Error Message
  var error = new InvalidInput<Person>()
    .Add(p => p.Address,
    "Services are currently unavailable in your area")
    .Add(p => p.Mobile,
    "Mobile is already registered");
 
  // ⚠️ Return Error If Person Is Invalid
  return BadRequest(error.InvalidProperties);
 
  // 👌 Continue...
}
[HttpPost]
public IActionResult CreateAccount([FromBody] Person person)
{
  // 🏗️ Build Error Message
  var error = new InvalidInput<Person>()
    .Add(p => p.Address,
    "Services are currently unavailable in your area")
    .Add(p => p.Mobile,
    "Mobile is already registered");
 
  // ⚠️ Return Error If Person Is Invalid
  return BadRequest(error.InvalidProperties);
 
  // 👌 Continue...
}

Output

The output of this method would be a BadRequest HTTP response with the error object as the Response body.

{
  "Address": "Services are currently unavailable in your area",
  "Mobile": "Mobile is already registered"
}
{
  "Address": "Services are currently unavailable in your area",
  "Mobile": "Mobile is already registered"
}

Properties > Generic Message

Returning properties in the error instead of just a generic message provides several benefits:

Specificity

When a user receives an error message, they expect it to be as specific as possible. By returning the properties that are invalid, the user can quickly identify which inputs are causing the error and take action to correct them.

Contextual information

When returning properties in the error, the user can see the context of the error in relation to the input data. This can help the user understand why the error occurred and how to fix it.

Debugging

When debugging, developers can use the properties in the error to identify the source of the problem more easily. They can also use this information to trace the error through the codebase.

Localization

By returning properties in the error, developers can create more specific error messages that can be localized for different languages. This improves the user experience for non-english speakers and helps them to understand the error message.

Automation

When the error message contains the specific properties that caused the error, it’s possible to automate the process of correcting the error by using the information to pre-populate the input fields.

Lambda > String

Using lambda expressions to get the name of a property provides several benefits:

Readability

Using a lambda expression to select a property is more readable than hardcoding the property name as a string. It makes the code more self-explanatory, and it’s clear to see which property is being selected.

Type safety

Since the lambda expression is strongly typed, it can catch errors at compile-time if the property name is mistyped or if the property does not exist on the type. This can prevent runtime errors and make the code more robust.

Refactor-friendly

If the property name is refactored (e.g. renamed), the lambda expression will automatically update to reflect the new name. This can save a lot of time and effort when making changes to the codebase.

Reusability

The InvalidInput<TSource> class is generic and can be used for any type, as long as the property names are selected using lambda expressions. This makes the class more versatile and reusable.