There are times in code when you need to return the results of an operation, but aren’t using a transport mechanism like HTTP that gives you a data structure with built-in statuses. In these situations, a lightweight OpResult class can be really helpful.

The original problem: returning when an operation fails

Imagine you’re writing a method to send a document to a printer. Your two teammates—let’s call them Karen and Ivan—will need it, so the method needs to be independent of any UI and reusable in different places. You don’t want to throw an exception just because the printer is turned off, so your first quick implementation looks like this:

Our first print method implementation

public bool PrintDocument(string filename, uint pageCount)
{
    try
    {
        var printer = LocalPrinters.GetCurrent(); // gets the computer's default installed printer            
        var jobResult = printer.Print(filename, pageCount);
        return true;
    }
    catch
    {
        return false;
    }
}

The next day Karen pings you that her printer will throw an error if one of the color ink cartridges needs to be replaced, but she can’t tell the user which one because we’re only returning a success/failure flag. Easy fix, right?

Adding support for error messages from exceptions

public string PrintDocument(string filename, uint pageCount)
{
    try
    {
        var printer = LocalPrinters.GetCurrent(); // gets the computer's default installed printer            
        var jobResult = printer.Print(filename, pageCount);
        return string.Empty; // no error occurred
    }
    catch (Exception ex)
    {
        return ex.Message; // return what cartridge is out of ink
    }
}

Later in the week, you hear from Ivan. He wasn’t as vigilant as Karen in preventing users from submitting print requests without all the needed information. He asks if your method can return all the information the user needs to fix while printing a file. Normally you’d tell him he needs to write better code, but he’s the company owner’s son and you like being employed. A quick adjustment later, you’ve solved the problem:

Adding support for parameter validation (multiple messages)

public List<string> PrintDocument(string filename, uint numCopies)
{
    var errors = new List<string>();

    // do user input validation before trying to print
    if (string.IsNullOrEmpty(filename))
        errors.Add("Filename must be specified");

    if (numCopies == 0)
        errors.Add("Must specify at least one copy to be printed");

    if (errors.Any()) return errors;

    try
    {
        var printer = LocalPrinters.GetCurrent(); // gets the computer's default installed printer            
        var jobResult = printer.Print(filename, numCopies);
    }
    catch (Exception ex)
    {
        errors.Add(ex.Message); // return what cartridge is out of ink
    }
    return errors;
}

The problem grows: returning more than just errors

A few days later we get another request from Karen. She’s handling financial reports and has a new requirement from a green initiative to track how many sheets of paper are getting printed. Uh-oh. We’re already returning something—errors. Your brain immediately conjures up a new class to combine the error results and requested data:

Custom class to return print results

public class PrintResult
{
    public int PagesPrinted { get; set; }
    public List<string> Errors { get; } = new List<string>();
}

But this isn’t your first rodeo. You know what’ll happen if you introduce this pattern into the code base–lots of similar Result classes with slightly different implementations. You mumble to yourself “there’s got to be a better way” as you go fix a fresh coffee and take a slow walk in the nearby park to clear your mind. After several sips and a couple of hundred steps of aimless wandering, the answer bubbles up from your subconscious.

Handling multiple needs with a single reusable approach

Generic Result Class

public class OpResult<T>
{
    public T Data { get; set; } = default;
    public List<string> Errors { get; } = new List<string>();
}

By using a generic for the result, this same class can be used for any situation where we need to gracefully return either a success result of some kind or a list of errors. Upon a little reflection, you add another property to make it easier for the calling code to check for success.

Result class with convenient success flag

public class OpResult<T>
{
    public bool Succeeded { get; }
    public T Data { get; set; } = default;
    public List<string> Errors { get; } = new List<string>();

    public OpResult(IEnumerable<string> errors, T data)
    {
        if (errors != null)
            Errors.AddRange(errors);

        Succeeded = Errors.Count == 0;

        Data = data;
    }
}

// method using OpResult
public OpResult<int> PrintDocument(string filename, uint numCopies)
{
    // parameter checks not shown
    try
    {
        var printer = LocalPrinters.GetCurrent();
        var jobResult = printer.Print(filename, numCopies);

        return new OpResult<int>(errors, jobResult.PagesPrinted); // return success
    }
    catch (Exception ex)
    {
        errors.Add(ex.Message);
        return new OpResult<int>(errors, 0); // return failure
    }
}

Lastly, you realize that you can simplify the object construction and ensure it’s always in a valid state by using factory methods to set the properties instead of making the developer set everything themselves. Factory methods with different names will also make it easier to see what kind of result is being created, as you can see by comparing the code lines commented below with “return success” and “return failure” with the previous snippet using a constructor.

Now, let’s take a look at the complete solution:

Complete OpResult IT implementation

public class OpResult<T>
{
    public bool Succeeded { get; }
    public List<string> Errors { get; } = new List<string>();

    public T Data { get; }

    protected OpResult(IEnumerable<string> errors, T data)
    {
        if (errors != null)
            Errors.AddRange(errors);

        Succeeded = Errors.Count == 0;

        Data = data;
    }

    public static OpResult<T> Success(T data)
    {
        return new OpResult<T>(null, data);
    }

    public static OpResult<T> Failure(string error)
    {
        return Failure(new[] { error });
    }

    public static OpResult<T> Failure(IEnumerable<string> errors)
    {
        if (errors == null)
            throw new ArgumentNullException(nameof(errors), "Creating OpResult failure requires at least one error, received null");

        var e = errors.ToArray();
        if (e.Length == 0)
            throw new ArgumentException("Creating OpResult failure requires at least one error, received empty", nameof(errors));

        return new OpResult<T>(e, default);
    }
}

// method using OpResult
public OpResult<int> PrintDocument(string filename, uint numCopies)
{
    // parameter checks not shown while focusing on using OpResult
    try
    {
        var printer = LocalPrinters.GetCurrent();
        var jobResult = printer.Print(filename, numCopies);

        return OpResult<int>.Success(jobResult.PagesPrinted); // return success
    }
    catch (Exception ex)
    {
        errors.Add(ex.Message);
        return OpResult<int>.Failure(errors); // return failure
    }
}

Key Takeaways

OpResult isn’t a silver bullet and shouldn’t be used everywhere. When you don’t have another mechanism (such as HTTP responses) available for easily returning operation errors and results together, though, a lightweight OpResult class helps you effortlessly and cleanly deal with errors and results without creating many similar, slightly different Result classes.

Have a tech-oriented question? We'd love to talk.