Design pattern for including errors with return values
Asked Answered
H

3

6

I'm writing an add-in for another piece of software through its API. The classes returned by the API can only be access through the native software and the API. So I am writing my own stand alone POCO/DTO objects which map to the API classes. I'm working on a feature which will read in a native file, and return a collection of these POCO objects which I can stole elsewhere. Currently I'm using JSON.NET to serialize these classes to JSON if that matters.

For example I might have a DTO like this

public class MyPersonDTO
{
    public string Name {get; set;}
    public string Age {get; set;}
    public string Address {get; set;}       
}

..and a method like this to read the native "Persons" into my DTO objects

public static class MyDocReader
{
    public static IList<MyPersonDTO> GetPersons(NativeDocument doc)
    {
        //Code to read Persons from doc and return MyPersonDTOs
    }
}

I have unit tests setup with a test file, however I keep running into unexpected problems when running my export on other files. Sometimes native objects will have unexpected values, or there will be flat out bugs in the API which throw exceptions when there is no reason to.

Currently when something "exceptional" happens I just log the exception and the export fails. But I've decided that I'd rather export what I can, and record the errors somewhere.

The easiest option would be to just log and swallow the exceptions and return what I can, however then there would be no way for my calling code to know when there was a problem.

One option I'm considering is returning a dictionary of errors as a separate out parameter. The key would identify the property which could not be read, and the value would contain the details of the exception/error.

public static class MyDocReader
{
    public static IList<MyPersonDTO> persons GetPersons(NativeDocument doc, out IDictionary<string, string> errors)
    {
        //Code to read persons from doc
    }
}

Alternatively I was also considering just storing the errors in the return object itself. This inflates the size of my object, but has the added benefit of storing the errors directly with my objects. So later if someone's export generates an error, I don't have to worry about tracking down the correct log file on their computer.

public class MyPersonDTO
{
    public string Name {get; set;}
    public string Age {get; set;}
    public string Address {get; set;}

    public IDictionary<string, string> Errors {get; set;}   
}

How is this typically handled? Is there another option for reporting the errors along with the return values that I'm not considering?

Hilbert answered 26/10, 2015 at 19:32 Comment(4)
Depends on implementation, Martin Fowler consider the last option the most valid. There's the Exception way as well, where you throw an exception when something goes wrong.Faubourg
Do you happen to have a link to a blog post or presentation where Martin talks about this?Hilbert
There goes the link for the post: martinfowler.com/articles/replaceThrowWithNotification.htmlFaubourg
You could do callbacks (delegates/lambda) or events as well.Sussi
D
5

Instead of returning errors as part of the entities you could wrap the result in a reply or response message. Errors could then be a part of the response message instead of the entities.

The advantage of this is that the entities are clean

The downside is that it will be harder to map the errors back to offending entities/attributes.

When sending batches of entities this downside can be a big problem. When the API is more single entity oriented it wouldn't matter that much.

Denaturalize answered 26/10, 2015 at 19:48 Comment(3)
This is a good option, but unfortunately I'm rarely working with single objects. The most typical use case is to compare a number of object exports from two separate files. I'm also almost always serializing the returned object to JSON, and dealing with that JSON later. So storing the errors separately would be problematic.Hilbert
I believe he meant you would return ParseResponse which is defined as public class ParseResponse { public IList<MyPersonDTO> Persons {get;set;} public IList<Exception> Exceptions {get;set;}} You can then serialize return.Persons and it would be free of any status/return code data.Sussi
Yes, that is what I meantDenaturalize
M
1

In principal, if something goes wrong in API (which cannot be recovered), the calling code must be aware that an exception has occurred. So it can have a strategy in place to deal with it.

Therefor, the approach that comes to my mind is influenced by the same philosophy -

1> Define your own exception Lets say IncompleteReadException. This Exception shall have a property IList<MyPersonDTO> to store the records read until the exception occurred.

public class IncompleteReadException : Exception
{
    IList<MyPersonDTO> RecordsRead { get; private set; }       

   public IncompleteReadException(string message, IList<MyPersonDTO> recordsRead, Exception innerException) : base(message,innerException)
    {
        this.RecordsRead = recordsRead;
    }
}

2> When an exception occurs while reading, you can catch the original exception, wrap the original exception in this one & throw IncompleteReadException

This will allow the calling code (Application code), to have a strategy in place to deal with the situation when incomplete data is read.

Mello answered 27/10, 2015 at 4:14 Comment(0)
M
0

Instead of throwing Exceptions throughout your code, you can return some extra information together with whatever you want to return.

    public (ErrMsg Msg, int? Result) Divide(int x, int y)
    {
        ErrMsg msg = new ErrMsg();
        try
        {
            if(x == 0){
                msg = new ErrMsg{Severity = Severity.Warning, Text = "X is zero - result will always be zero"};
                return (msg, x/y);
            }
            else
            {
                msg = new ErrMsg{Severity = Severity.Info, Text = "All is well"};
                return (msg, x/y);
            }
        }
        catch (System.Exception ex)
        {
            logger.Error(ex);
            msg = new ErrMsg{Severity=Severity.Error, Text = ex.Message};                
            return (msg, null);
        }            
    }
Misbegotten answered 30/5, 2022 at 23:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.