Author: Brandon Pearman

The views expressed here are mine alone and do not reflect the view of my employer.

When you call a method, the calling code needs to know if it was successful or not. For example, let's assume we have a business logic method called Add(Book book) which Adds data to a database but does not need to return data:

There are many ways to do this...

  1. Bools
  2. Nulls
  3. Complex objects
  4. Exceptions

We will cover what each of these look like as well as their pros and cons. We will consider three cases:

  1. Failure case: the book input is null (http client needs to detect this and return 400)
  2. Failure case: adding to the database fails (http client needs to detect this and return 500)
  3. Success case: adding to the database succeeds (http client needs to detect this and return 200)

One's first instinct would be to add a bool so you could check if the call succeeded.

public bool Add(Book book)
{
    if(book == null)
    {
        return false;
    }

    bool addSucceeded = _bookRepo.Add(book);
    if(addSucceeded)
    {
        return true;
    }
    else
    {
        return false;
    }
}

In the above example we could simply "return addSucceeded;" but in many cases where you need to do more work after the db call it will look closer to the above.

How the HTTP API client would use the method:

[HttpPost]
public IActionResult Add(Book book)
{
    bool addSucceeded = _bookService.Add(book);
    if(addSucceeded)
    {
        return Ok();
    }
    else
    {
        return BadRequest();
    }
}

Pros Á Cons

  • Con: The return type of bool does not provide much information about what happened in the method. Since it is only true or false we can not distinguish between different returns. eg for our error cases we have to choose between status code 400 and 500.
  • Con: Prone to false positives. If a developer forgets to check the bool a bug can slip in. This happens more often in more complex code.
  • Con: This strategy can not be used universally because some methods will need to return data.
  • Pro: Simple logic
  • Pro: Return type is easy to test and use

A very similar concept to the bollean concept, is to return an object and check for nulls.

public Book Add(Book book)
{
    if(book == null)
    {
        return null;
    }

    bool addSucceeded = _bookRepo.Add(book);
    if(addSucceeded)
    {
        return book;
    }
    else
    {
        return null;
    }
}

If you do not want to return the data like which is done in the above example, you can return an empty class. The below example shows returning a simpe Response object which is empty but you easily add to later if need.

public AddRespose Add(Book book)
{
    if(book == null)
    {
        return null;
    }

    bool addSucceeded = _bookRepo.Add(book);
    if(addSucceeded)
    {
        return new AddRespose();
    }
    else
    {
        return null;
    }
}

How the HTTP API client would use the method:

[HttpPost]
public IActionResult Add(Book book)
{
    book returnBook = _bookService.Add(book);
    if(returnBook != null)
    {
        return Ok();
    }
    else
    {
        return BadRequest();
    }
}

Pros Á Cons

  • Con: Using nulls for errors is similar to the return type of bool because it also does not provide much information about what happened in the method. Since it is only null or not null we can not distinguish between different returns. eg for our error cases we have to choose between status code 400 and 500.
  • Con: Prone to false positives. If a developer forgets to check the null a bug can slip in. This happens more often in more complex code.
  • Pro: This strategy can be used universally because regardless of what data the method needs to return it can be added to the response object.
  • Pro: Simple logic
  • Pro: Return type is easy to test and use

Instead of returning an empty class, you could put the error message in the class.

public class AddResponse
{
    public ErrorCode ErrorCode { get; set; }
    public string ErrorMessage { get; set; }
    public Book Book { get; set; }
}
publicEnum ErrorCode
{
    InvalidInput,
    DbError
}

Note I created an ErrorCode enum to be more explict about the error and to prevent the business logic from being aware of the clients technology. eg the same business logic can be used on a client using HTTP, AMQP, Console, etc.

public AddResponse Add(Book book)
{
    if(book == null)
    {
        return new AddResponse()
        {
            ErrorCode = ErrorCode.InvalidInput,
            ErrorMessage = "Book input is empty",
            Book = null
        };
    }

    bool addSucceeded = _bookRepo.Add(book);
    if(addSucceeded)
    {
         return new AddResponse()
        {
            ErrorCode = None,
            ErrorMessage = null,
            Book = book
        };
    }
    else
    {
        return new AddResponse()
        {
            ErrorCode = ErrorCode.DbError,
            ErrorMessage = "Add to Database failed",
            Book = null
        };
    }   
}

How the HTTP API client would use the method:

[HttpPost]
public IActionResult Add(Book book)
{
    AddResponse addResponse = _bookService.Add(book);
    if(addResponse.ErrorCode == ErrorCode.None)
    {
        return Ok(addResponse.Book);
    }
    else if(addResponse.ErrorCode == ErrorCode.InvalidInput)
    {
        return BadRequest(addResponse.ErrorMessage);
    }
    else if(addResponse.ErrorCode == ErrorCode.DbError)
    {
        return InternalServerError(addResponse.ErrorMessage);
    }
    else
    {
        return InternalServerError(addResponse.ErrorMessage);
    }
}

Pros Á Cons

  • Con: Having multiple return types leads to more required checks and difficult maintenance.
  • Con: Prone to false positives. If a developer forgets to check the return object or checks it incorrectly a bug can slip in. This happens more often in more complex code.
  • Pro: This strategy can be used universally because regardless of what data the method needs to return it can be added to the response object.
  • Pro: Can return more detailed error messages because it allows for multi return error types.
  • Pro: Return type is easy to test and use (if the error codes are clear )

False positives:With this particular strategy false positives are particularly bad. There are some cases which should never happen but since there is no enforcment, they could happen.

  • Con: What if the response object is null? We need null checks as well.
  • Con: What if the response object was not populated? The error enum will default to 0 telling the client it was successful (we should probably change the default success code to maybe 200). We will still need to check for the 0 code then as an edge case.

The amount of checks required for normal cases and edge cases can lead to bugs if developers miss something.

You can create a method which has all these checks but you will still have to remember to trigger that code. New developers could easily miss that.

Another strategy is to return whatever you want but check errors through exceptions. With the strategy it is best to create custom exceptions like this:

public class InvalidInputException : Exception
{
    public ClientException(string errorMessage) : base(errorMessage)
    {
    }
}

You can then throw different types of exceptions in your method and they work like returns which bubble all the way up unless caught.

public void Add(Book book)
{
    if(book == null)
    {
        throw new InvalidInputException("Book input is empty");
    }
    
    bool addSucceeded = _bookRepo.Add(book);
    if(addSucceeded == false)
    {
        throw new DbErrorException("Add to Database failed");
    }   
}

How the HTTP API client would use the method:

[HttpPost]
public IActionResult Add(Book book)
{
    try
    {
        AddResponse addResponse = _bookService.Add(book);
        return Ok();
    }
    catch(InvalidInputException ex)
    { 
        return BadRequest(ex.Message);    
    }
    catch(DbErrorException ex)
    {
        return InternalServerError(ex.Message);
    }
    catch(Exception ex)
    {
        return InternalServerError(ex.Message);
    }
}

Since the exception will bubble up all the way you can add error handling middleware / global catches, to make your controllers really simple.

[HttpPost]
public IActionResult Add(Book book)
{
    AddResponse addResponse = _bookService.Add(book);
    return Ok();
}

Pros Á Cons

  • Con if no middleware: Having multiple return types leads to more required checks and difficult maintenance.
  • Con if middleware: Harder to read what that particular end point may return.
  • Pro if middleware: Easy to maintain.
  • Pro: NO false positives. If a developer forgets to catch the exceptions it will still bubble up and prevent any bugs slipping in. eg anything calling this end point will get 500 status codes for failure cases instead of 200.
  • Pro: This strategy can be used universally because regardless of what data the method needs the errors are thrown.
  • Pro: Can return more detailed error messages because it allows for multi return error types.
  • Pro: Return type is easy to test and use (if the custom exceptions are clear. Note: in unit test success is not checked but instead is simply the lack of any exceptions)

For POCs and simpe projects it is fine to go with the bool or nulls but I would generally NOT recommend them.

For any real work being done I would recommend using the complex objects or exceptions from the start.

My personal preference is to throw exceptions and use middleware, due to the safety they provide as well as the ease and cleanliness.

Check out these links for more info:

My design and architecture repo