Why Result Objects Are Preferred Over Exceptions

As a C# developer, you’ve likely encountered exceptions countless times throughout your career. They are a fundamental aspect of error handling in many object-oriented programming languages, including C#.

sharmila subbiah
.Net Programming
5 min readAug 9, 2024

--

However, as software engineering evolves, particularly with the growing influence of functional programming paradigms, the concept of Result objects (often referred to as Result<T>) is gaining traction as an alternative approach to error handling. This method challenges the traditional use of exceptions, offering distinct advantages in specific contexts.

In this article, we’ll explore why, despite the widespread use of exceptions, Result objects are increasingly favoured in certain scenarios. We’ll discuss their benefits and drawbacks, and examine situations where adopting a Result-based approach could make your codebase more robust, maintainable, and easier to understand.

Understanding Exceptions in C#

In C#, exceptions signal that an error has occurred during program execution. When an exception is thrown, the program’s normal flow is interrupted, and control is passed to the nearest exception handler, typically within atry-catch block. This method is intuitive and deeply integrated into the C# language, making it a powerful tool for handling errors.

Advantages of Exceptions

  • Separation of Concerns: Exceptions enable you to separate error-handling code from the main business logic, making your methods cleaner and easier to read.
  • Flexible Handling: Since exceptions can propagate up the call stack, they offer a flexible mechanism for handling errors at different levels of your application.
  • Built-in Support: C# provides extensive built-in support for exceptions, including standard exception types, the throw keyword, and try-catch-finally constructs.

Drawbacks of Exceptions

  • Control Flow Disruption: Exceptions interrupt the normal flow of execution, which can complicate the code and make it harder to maintain. This disruption often leads to tangled logic, particularly in complex systems.
  • Performance Overhead: Throwing and catching exceptions incurs a performance cost, including stack unwinding and the creation of exception objects, which can be significant in performance-critical applications.
  • Implicit Error Handling: With exceptions, it’s not always obvious which parts of your codebase might throw errors. This implicitness can result in unhandled exceptions, bugs, or the need for extensive documentation and testing to ensure reliability.

Introduction to Result Objects

A Result object is a type that explicitly represents either a successful outcome or a failure. Instead of throwing exceptions, a method can return a Result<T> (or a similar structure), where T represents the type of a successful result. If the operation fails, the Result object contains an error message or code, but not an exception.

Here’s a basic example of a Result object implementation in C#:

public class Result<T>
{
public T Value { get; }
public string Error { get; }
public bool IsSuccess => Error == null;

private Result(T value, string error)
{
Value = value;
Error = error;
}

public static Result<T> Success(T value) => new Result<T>(value, null);
public static Result<T> Failure(string error) => new Result<T>(default, error);
}

Advantages of Result Objects

  • Explicit Error Handling: Result objects clearly indicate in the method signature that an operation can fail. This explicitness compels developers to address both success and failure cases, reducing the likelihood of unhandled errors.
  • No Control Flow Disruption: Unlike exceptions, Result objects don’t disrupt the normal execution flow, making your code easier to follow, test, and maintain.
  • Predictability and Type Safety: Since the possibility of failure is part of the function’s return type, it’s easier to understand where and how errors might occur, which is particularly beneficial in complex systems.
  • Composability: Result objects can be composed using functional programming techniques like map, flatMap (or Select, SelectMany in LINQ), and pattern matching, resulting in cleaner and more expressive code.
  • Performance: Result objects avoid the overhead associated with exceptions, offering better performance in scenarios where error handling is frequent.

Drawbacks of Result Objects

  • Verbosity: Result objects can lead to more verbose code since every potential error must be explicitly handled. This verbosity can feel cumbersome, especially for developers accustomed to the brevity of exception handling.
  • Boilerplate: Without language-level support, Result objects can introduce boilerplate code, particularly in the absence of pattern matching.

When to Prefer Result Objects Over Exceptions

While exceptions are suitable for truly exceptional situations (e.g., unexpected conditions that should rarely occur), Result objects excel in scenarios where failures are a regular part of business logic. Consider using Result objects in the following cases:

  • Business Logic Operations: In domain-driven design (DDD) or similar practices, Result objects can make business logic flows more explicit and predictable, such as when processing user input or validating domain invariants.
  • Microservices and APIs: In microservices and web APIs, using Result objects can lead to more predictable and reliable service interactions. Instead of converting caught exceptions into HTTP responses, you can return a Result object that the API layer can easily transform into an appropriate response.
  • Functional Programming: Result objects align well with functional programming concepts like immutability, higher-order functions, and monads, enabling cleaner and more expressive code.
  • Performance-Critical Code: In performance-critical applications, avoiding the overhead of exceptions is crucial. Result objects provide a lightweight alternative that can improve performance without sacrificing error handling.
  • Complex Systems with Deep Call Stacks: In systems with deep call stacks, exceptions can lead to stack unwinding and complicate debugging. Result objects maintain a linear and predictable flow, simplifying both debugging and testing.

Practical Example: Refactoring with Result Objects:

Original Method Using Exceptions

public decimal Divide(decimal dividend, decimal divisor)
{
if (divisor == 0)
{
throw new DivideByZeroException("Cannot divide by zero.");
}
return dividend / divisor;
}

try
{
var result = Divide(10, 0);
Console.WriteLine($"Result: {result}");
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}

Refactored Method Using Result

public Result<decimal> Divide(decimal dividend, decimal divisor)
{
if (divisor == 0)
{
return Result<decimal>.Failure("Cannot divide by zero.");
}
return Result<decimal>.Success(dividend / divisor);
}

var result = Divide(10, 0);
if (result.IsSuccess)
{
Console.WriteLine($"Result: {result.Value}");
}
else
{
Console.WriteLine($"Error: {result.Error}");
}

In the refactored method, the division operation now explicitly handles cases where the divisor is zero without disrupting control flow or relying on exceptions. This approach is more predictable and integrates seamlessly with the rest of the codebase.

Example Usage with Multiple Methods

Here’s how you might handle more complex scenarios with multiple methods using Result objects.

public Result<decimal> CalculateTax(decimal amount, decimal taxRate)
{
if (taxRate < 0 || taxRate > 1)
{
return Result<decimal>.Failure("Invalid tax rate.");
}
return Result<decimal>.Success(amount * taxRate);
}

public Result<decimal> CalculateTotal(decimal amount, decimal taxRate)
{
var taxResult = CalculateTax(amount, taxRate);
if (!taxResult.IsSuccess)
{
return Result<decimal>.Failure(taxResult.Error);
}

return Result<decimal>.Success(amount + taxResult.Value);
}

var totalResult = CalculateTotal(100m, 0.05m);
if (totalResult.IsSuccess)
{
Console.WriteLine($"Total Amount: {totalResult.Value}");
}
else
{
Console.WriteLine($"Error: {totalResult.Error}");
}

Summary

While exceptions remain a powerful tool in the C# developer’s toolkit, Result objects offer a compelling alternative for specific scenarios. By making error handling explicit, predictable, and more in line with functional programming practices, Result objects can lead to cleaner, more maintainable code. That said, the choice between exceptions and Result objects is context-dependent.

--

--

sharmila subbiah
.Net Programming

With over a decade of experience in the tech industry, I currently hold the position of Senior Software Engineer at Youlend.