C# different return types based on try-catch

I'm calling API in one of my Service classes as below:

public GetCustomerApiResponse SearchCustomerByEmail(string email)
        {
            GetCustomerApiResponse customerApiResponse = null;
            try
            {
                var requestBody = JsonConvert.SerializeObject(new
                {
                    content = $@"{{""Email"":""{email}""}}",
                    key = _apiEnvironment,
                    name = "search-customer"
                });

                var content = new StringContent(requestBody, Encoding.UTF8, "application/json");
                var res = _client.PostAsync(_baseUrl, content).Result.Content.ReadAsStringAsync().Result;
                customerApiResponse = JsonConvert.DeserializeObject<GetCustomerApiResponse>(res);
            }
            catch
            {
                // ignored
            }

            return customerApiResponse;
        }

Here, I'm returning Deserialized data of type GetCustomerApiResponse. I want to return Different Response Type in catch (say e.g. of type GetCustomerApiResponseError). How to achieve this?


Presuming you want to do something if the exception is thrown I'd reccomend you do something more like this:

public GetCustomerApiResponse SearchCustomerByEmail(string email)
{
   try {}
   catch(Exception ex)
   {
       throw MyException(ex);
   }
   return customerApiResponse;

}

Where MyException looks like this:

public MyException : Exception
{

   public MyException(Exception innerException) : base(innerException) {}
}

and the calling code looks like this:

try
{
    GetCustomerApiResponse response =  SearchCustomerByEmail(email);
}
catch(MyException ex)
{
     //do whatever you need to happen if it fails.
}

This way the calling code is aware that an exception is thrown (no hidden functionality here) but you can also handle that exception in an elegant way. You can extend MyException if you need to provide more information for your handling logic.


Unfortunately you cannot return different types unless you're dealing with a base type.

If you want to handle the error in a specific way other than letting the Exception be handled by calling code, you could return a GetCustomerApiResult

public class GetCustomerApiResponse {

    GetCustomerApiResponse CustomerApiResponse { get; set; }

    GetCustomerApiResponse CustomerApiError { get; set; }
}

Then you can do

public GetCustomerApiResult SearchCustomerByEmail(string email)
{
    var result = new CuscustomerApiResult();
    try
    {
        ...

        customerApiResult.CustomerApiResponse = JsonConvert.DeserializeObject<GetCustomerApiResponse>(res);
    }
    catch
    {
        // Handle the exception but return some error information
        customerApiResult.CustomerApiError = ...
    }

    return customerApiResponse;
}

And in your calling code you can check whether customerApiResult.CustomerApiError is not null for an error or check customerApiResult.CustomerApiResult for success.


First, as a rule, you really shouldn't ignore (A.K.A. swallow) exceptions.

swallowing exceptions is considered a code smell and usually frowned upon for good reasons.

That being said, There are cases when you don't want or don't need to throw an exception - but still inform the calling method that you've encountered some problem and can't deliver the result it expect.

Such a use-case might be when you're attempting to validate some complex data you've received from a service - making sure, for example, all the properties you need actually have values you can work with (none of them are default or something like that).
An "invalid" data might be expected to be returned from the service and therefor it might not be best practice to throw an exception in such a case - For these situations I like to use what I've called a Result<T>.

The Result<T> a wrapper class over the value that should be returned otherwise - with a couple more properties - a bool indicating success or failure and a string optionally containing an error description.

In your case, the code would look like this:

public Result<GetCustomerApiResponse> SearchCustomerByEmail(string email)
{
    try
    {
        var requestBody = JsonConvert.SerializeObject(new
        {
            content = $@"{{""Email"":""{email}""}}",
            key = _apiEnvironment,
            name = "search-customer"
        });

        var content = new StringContent(requestBody, Encoding.UTF8, "application/json");
        var res = _client.PostAsync(_baseUrl, content).Result.Content.ReadAsStringAsync().Result;
        
        return Result<GetCustomerApiResponse>
            .Success(JsonConvert.DeserializeObject<GetCustomerApiResponse>(res));
    }
    catch(Exception ex)
    {
        return Result<GetCustomerApiResponse>
            .Fail(ex.Message); // or ex.ToString() if you need more information
    }
}

The calling code will do something like this:

var result = SearchCustomerByEmail(email);
if(!result.Succeeded)
{
    Log(result.ErrorDescription);
    // Perhaps show the error description to the user (if you have one)
    return;
}
var customer = result.Value;
... rest of the code here

You can read more about it in Handling non exceptional errors using Result and Result on my blog.


I'd use a class CustomerApiResult which has a property GetCustomerApiResponse and also ValidResponse + IEnumerable<string> ResponseErrors.

public class CustomerApiResult
{
    // make the constructor private and use factory methods as below
    private CustomerApiResult(
        bool validResponse, 
        GetCustomerApiResponse apiResponse,
        //... other properties..., 
        string invalidReason,
        IEnumerable<string> responseErrors)
    {
        ValidResponse = validResponse;
        CustomerApiResponse = apiResponse;
        // other properties ...;
        InvalidReason = invalidReason;
        ResponseErrors = responseErrors ?? Enumerable.Empty<string>();
    }

    public bool ValidResponse { get; }
    public GetCustomerApiResponse CustomerApiResponse { get; }
    // other properties
    public string InvalidReason { get; }
    public IEnumerable<string> ResponseErrors { get; private set; }

    public static CustomerApiResult Valid(
        GetCustomerApiResponse apiResponse,
        // other properties, 
        IEnumerable<string> responseErrors = null)
           => new CustomerApiResult(true, apiResponse, null, responseErrors);

    public static CustomerApiResult Invalid(
        GetCustomerApiResponse apiResponse,
        // other properties, 
       string invalidReason = null, 
       IEnumerable<string> responseErrors = null)
          => new CustomerApiResult(false, apiResponse, invalidReason, responseErrors);
}

Now you can use it in this way:

public CustomerApiResult SearchCustomerByEmail(string email)
{
    try
    {
        var requestBody = JsonConvert.SerializeObject(new
        {
            content = $@"{{""Email"":""{email}""}}",
            key = _apiEnvironment,
            name = "search-customer"
        });

        var content = new StringContent(requestBody, Encoding.UTF8, "application/json");
        var res = _client.PostAsync(_baseUrl, content).Result.Content.ReadAsStringAsync().Result;
        
        return CustomerApiResult.Valid(JsonConvert.DeserializeObject<GetCustomerApiResponse>(res));
    }
    catch(Exception ex)
    {
        return CustomerApiResult.Invalid(null, $"Could not convert to {nameof(GetCustomerApiResponse)}", new[]{ex.Message});
    }
}

So i don't return the converted object but an API-Result object that contains the object(or null) and has a ValidResponse property that you should check before.

So for example:

CustomerApiResult result = _api.SearchCustomerByEmail("[email protected]");
if(result.ValidResponse)
{
    GetCustomerApiResponse customer = result.CustomerApiResponse;
    // do something with the customer ...
}
else
{
    // log the response errors and/or the InvalidReason and inform the user
}

One of the advantages of this approach is that you can use the logic even if there are no exceptions raised(so for example there is an invalid result from the api or a logical flaw). Another advantage is that you can use it with multiple consecutive calls in the API before you return to the upper application layers and collect the response-errors(for example to log them separately). You can also use this to decide if you can continue with the next step if there is already an invalid reponse in the last call or if that call was not important enough to stop the whole workflow.