Error return values for functions that call external system

Hey,

in my project, I got quite some functions that do 2 calls to an external system and in the best case, return {:ok, result} back.

In a nutshell:

with {:ok, result1} <- query_service_1(args),
     {:ok, result2} <- query_service_2(result1[:id]) do
  result2
else
  err -> {:error, err}
end

The 2 external services are called with HTTPoison and therefore can return all sorts of HTTP error codes, or other errors (when there is no connection to the service, etc.)

The functions in the example (query_service_1) are sometimes even just plain HTTPoison.get function calls.

Now I struggle with figuring out a good way to handle all these possible errors to give a meaningful error response, both for further code and the response to the user. Maybe I need a custom error struct that holds all information one could need to interpret what went wrong.

I would be grateful for any advice.

1 Like

:wave:

You might want to consider using Sage if it’s not an overkill.

1 Like

I’ve seen it before and I think it’s a bit overkill for my case.

One possible starting point:

with {:service_1, {:ok, result1}} <- {:service_1, query_service_1(args)},
     {:service_2, {:ok, result2}} <- {:service_2, query_service_2(result1[:id])} do
  result2
else
  err -> error_result(err)
end

def error_result({:service_1, err}) do
# ...
end

def error_result({:service_2, err}) do
# ...
end

So error_result/1 is responsible for populating your “custom error struct” but at least you can deal with each possible stage of failure separately.

That is one possible solution. In fact, I am using that technique in another project already.
That still left a question:

  • What would query_service_1 return as an error value? Plain HTTPoison Response (e.g. when 404 status) or Error (e.g. when address not reachable) structs?

Maybe a custom error struct that gets returned by query_service_1 and therefore also by the with statement is the way to go.

Something in the lines of:

defmodule MyApp.RemoteServiceError do
  defstruct context: nil, # atom, eg.  :authenticate
            path: nil,
            method: nil,
            headers: nil,
            body: nil,
            response: nil,
            error: nil,
end

Then it will contain info about the request to the remote service. response & error are used depending on what HTTPoison returns.

If that is what you need …

I was more expecting a mapping of error in formation from the HTTP domain to an error classification that is more useful within your domain. Right now your error information is coupled to HTTP being the transport rather than translating what this particular failure means to your application as a whole.

1 Like

That is correct. The question is, where do you start and where do you end.
I could do specialized structs for certain error responses (401, 403, 404) but what if the remote service returns something totally unexpected? How would you fit that into a special error struct or should there be a fallback struct for general errors?

Some general thoughts:

  • The error information should focus on the detail 1) that the application can take action on 2) that is relevant to an attendant user/administrator for immediate action.

  • If the detailed HTTP information isn’t relevant to the application’s decision making process then it is probably sufficient to log the information for possible diagnosis later.

Now if the information can’t be logged immediately then it makes more sense to structure the information for logging (and then include it in the error). For that you only need a log level and a chardata field - though you may want to think about the organization within the chardata.

  • An application by definition can’t deal with unexpected errors. That’s what “let it crash” is all about. It can be useful to capture contextual information via the log - however there is little point including details in the error itself.
1 Like

Maybe I should describe my exact use case a bit more.
I am building a gateway between a mobile app and the backend. So the mobile app calls the gateway, the gateway calls the backend and returns the response to the app, while saving part of the response for certain other things meant to happen in the gateway.

So I guess one could say that “HTTP” is part of my application domain.
The “returns the response to the app” part would theoretically not need a sense of an “error” since it just passes through whatever came back but the “while saving part of the response for certain other things meant to happen” part needs to know if a certain action succeeded or not. I also need to log all error for debugging purposes (backend behaves in unexpected ways sometimes).

That is typically not the case - HTTP is just a transport.

since it just passes through whatever came back

That is not what your opening post states.

You are getting a single request from the mobile application and making multiple requests on the client’s behalf. That is Façade functionality, i.e. you are providing a simplified interface for the mobile client.

Part of that simplified interface is “translating” the errors to something that makes sense to the client application without necessarily revealing the details of the interactions that are going on inside the gateway.

This is equivalent to not showing all the potentially revealing details of a server error on a web page to a user.

So you still need to design the errors “client-first”; i.e. consider the nature of the request the client is making and determine from the client/request perspective what typically can go wrong with producing the desired response.

Once you have designed those “client errors”, you need to map any remote service errors to those client errors.

The status codes come in the broad categories of success, client-error, server-error.

  • For example, a remote service 500 server error can become a gateway 502 server error.
  • Client errors are the most difficult because the details of the interactions may necessitate that it is presented as a server error to the mobile client (because there is nothing the mobile client can do to fix the situation) while at other times a more specific client-error received by the gateway is turned into a more general one for the mobile client.

For more details have a look at:

API Facade Pattern - Errors page 11
Web API Design - Handling Errors - page 10

1 Like