Sending ok/error-tuples to the frontend - what do you use and why?

Recently on the Elm forums, a discussion about Union Types and how they are represented in other languages was started.

After telling them about the {:ok, value} | {:error, error_message} ‘type’ that sees frequent use in Elixir to represent error cases (AKA the Result or Either monad), someone had an interesting question: How do we represent these tuples when sending stuff to the frontend? (direct link to this post

Tuples themselves do not map to JSON; Poison.encode({:ok, "foo"}) will fail.

Some possibilities could be:

  1. tuple as an object
{:ok, 1} -> {"tag": "ok", "payload": 1}
{:error, 2} -> {"tag": "error", "payload": 2}
  1. tuple as a two-value array
{:error, 2} -> ["error", 2]
  1. discarding error messages, converting to a nullable type
{:ok, 1} -> 1
{:error, 2} -> null

Have you used any of these or another one, and why?

4 Likes

Personally I tend to do it like 1, also in my company we do that (though not elixir).

Our naming is a bit different though.

We tend to do it more like {"status": "ok", "data": …}/{"status": "error", "reason": …}. "reason" can, depending on how well defined the API is, be a plain string for human consumption or an object containing a numeric error code, a severity and free form “args”. But then always a human readable string is provided as well.

{
  "status": "error",
  "reason": {
    "code": 404,
    "description": "Resource 'foo' not found",
    "args": { "resource": "foo" }
  }
}
8 Likes

Why not use proper HTTP error code (depends on “meaning” of :error) and then passing second part of the tuple as a body?

2 Likes

Because errors in your application do not map to HTTP status codes very well.

My example with the resource not found was just an example. But if the user provided some value out of bounds, what HTTP status code would you use?

Also, if you say 404 is resource not found, how do you distinguish from the HTTP resource not beeing found vs. some resource not found which you specified in your arguments to the request?

3 Likes

HTTP 422

In both cases you haven’t found resource that met given attributes, doesn’t matter if this is id not matching nor name. Only exception from this rule is often when you implement searching, then request succeeded, it just returned no matches.

1 Like

Another good reason is because you are then conflating your own API layer with the transport protocol layer.

What if you ever want to expose the same API over a different transport protocol? (Like, for instance, websockets?) If you only use HTTP status codes to differ between success/error results, you will not be able to expose the same API over another protocol.

3 Likes

Then you will need implement different error handling in almost all cases. Because a lot of protocols implement their own error handling system. And even then all you need is to deencapsulate data simply by unwrapping data field from error structure. So API will be almost the same with exception to transport protocol, which you have changed anyway, so you need ot handle it in different way.

1 Like

Apologies in advance: this post is a little off-topic from the title of this thread… but it’s in the spirit of the original discussion on the Elm forums, which was was a little broader than :ok/:error tuples. We were discussing modelling union types as tagged tuples in general, and how they can be represented in different languages/notations. So not just modelling error states (that might correspond to HTTP status codes), but modelling any kind of state.

Imagine you are using tagged tuples to model the state of some kind of game, like:

{:not_started, %{p1_name: "Laurel", p2_name: "Hardy"}}
{:in_play, %{p1_name: "Laurel", p1_score: 0, p2_name: "Hardy", p2_score: 2}}
{:game_over, %{winner_name: "Laurel", winner_score: 10}}

If you wanted to send one of these states to your front-end via JSON, given that JSON lacks the concept of tuples, how would you encode it and why?

2 Likes

I think that a good REST API should indicate success/failure via the response status code. So returning 20x when a request has in fact failed (even if it was due to a business error) would be IMO misleading. If HTTP status codes are used, then success/failure can be distinguished on the client side by looking into the response status.

I do agree that HTTP error codes are probably too coarse grained for business errors, so providing additional context as a payload is fine.

This depends on the transports which have to be supported. For example, at Aircloak besides HTTP, we also support PostgreSQL protocol (we pretend to be a PostgreSQL database). Error reporting in that protocol has to be done in a particular way, so it’s still transport specific. Having the same shape of response sent via HTTP and via PostgreSQL protocol wouldn’t work for us.

4 Likes

We actually also use codes with extra info for HTTP or for WS we use Phoenix’ lib that lets you kind of match on the tuples, in Elixir something like this:

{:ok, thing} -> {:reply, {:ok, %{thing: thing}}, socket}
{:error, reason} -> {:reply, {:error, %{reason: reason}}, socket}

and in the JS API service:

channel.push(topic, data)
    .receive("ok", (resp) => {
      resolve(resp)
    })
    .receive("error", (resp) => {
      reject(new Error(resp.reason))
    })
    .receive("timeout", () => {
      reject(new Error(`Request timed out for ${topic}`))
    })
1 Like

Last time I built in errorhandling for a customer I did it this way (not in elixir either):
System and application errors with certain error numbers lead to a clientside error presentation with a message like: “An error occurred while processing x, contact y datetime”.
Application errors with other errornumbers lead to a presentation of a for a customer readable message + the errornumber.
Serverside the errors are logged in a database, with a stacktrace and some more, as are starting times of process steps etc. This logging is available for the helpdesk with a gui.
You could think of all kinds of extra’s like realtime notifying the helpdesk (if your application is not too unstable :slight_smile: ), the helpdesk trying to set up a chat with the customer “what have you been doing, we will fix this asap blabla” etc.

2 Likes

I use something that’s not quite unlike OmniTI Labs · GitHub when I have to work with untyped APIs.

It also has some notes about HTTP status codes / in-body status codes, from the link above:

it is advised that server-side developers use both: provide a JSend response body, and whatever HTTP header(s) are most appropriate to the corresponding body.

1 Like

Architecturally, that strikes me as the wrong answer. The more you have to change your application because you changed your transport layer, the more you should be thinking that you made a mistake in your design.

1 Like

I usually use this https://www.jsonrpc.org/specification#response_object when need some simple, yet standardized(ish), way to send success/error responses.

I use already defined standards for API responses:

for errors i use problem+json: https://tools.ietf.org/html/rfc7807
for normal responses I use HAL: http://stateless.co/hal_specification.html

If you need to change logic, then yes, I agree. However this isn’t the point. If you do not want to change any code and change transport layer (not representation, whole layer) then why we have whole different components for handling sockets instead of changing single function? Any transport layer will contain error handling, so you should utilise it for handling errors, but you shouldn’t leak that into your logic layer.

Having to change code because of a change in a different layer of the application shows that you have coupling between layers.

Is that coupling necessary? Maybe. Sometimes your transport layer is unavoidably coupled with a higher layer. But the suggestion of using HTTP error handling for a higher layer is just needless coupling. Keep that stuff separate from the beginning. Or don’t. :slight_smile:

1 Like

We went down a different path. We’re deprecating all our REST APIs and replacing them with GraphQL. The only status codes you get are 500, 404 (whoops) and 200. Any errors in the server should be returned as part of the GraphQL response. Having GrapiQL available to experiment and lookup things is really useful and apollo-client works well on the front end.

Using GraphQL has been worth the effort for us.

1 Like