Should context functions return a tuple? Or just the raw response?

As I write my contexts that tie into my Ecto Schemas, I am constantly second-guessing myself: what should these functions return? E.g. should a search function return {:ok, []} or simply []? Should a get operation return {:ok, the_thing} or just the thing?

As I start learning Absinthe and GraphQL, it seems like the resolvers are pegged to handle and repackage some of that output – am I overthinking it if I trap any errors and normalize my outputs at the context layer?

Thanks for your thoughts/opinions!

1 Like

If it can fail, return a tuple. If it can’t fail, just return the value.

10 Likes

Exactly this!

So in your example:

The search function probably should always return a list, because ‘no results’ is a valid successful search response.

The get function however might either be able to retrieve a value, or it might not (because no value with the given key exists). In this case, it makes sense to return an ok/error tuple, because it makes it explicit to users of the code that the function might sometimes fail.

Am I so pessimistic to assume that EVERYTHING can fail? E.g. I am using UUID binary_id’s as my primary keys – a get or get_by operation throws an error if you pass it an invalid input. I guess the question be framed to ask more what might reasonably fail.

If an function might fail during ‘normal operation’, it is better to return an ok/error-tuple to make it explicit that failures will commonly happen, to make the consumer of the code think of it.

If a function will only fail in exceptional circumstances, then it is usually better to raise an exception instead; these are the cases where ‘let it crash’ is the most appropriate default. The idea here is that these kinds of failures are implicit, which is okay since they are rare, so your consumer does not need to keep them in mind. In the rare, exceptional case that the consumer happens to trigger them (and triggers them so often that they become annoying), then they can mitigate them by rescuing them.
Do note that it is good form to list what kind of exceptions might be returned and why (both in the documentation as well as in the exception’s error message), so the user is able to find the information if they are looking for it.

In the case that a consumer of your code does not hold themselves to the preconditions of your function (such as passing in an argument of the wrong type), then you should immediately raise an error, such as an ArgumentError. After all, there is nothing sensible your function can do when it is improperly called. (It would be even better to refuse to compile the application, but this is not possible in a dynamic language, so raising at runtime immediately after calling it improperly is the best we can do.)

Tl;Dr:

  • Make your errors explicit by returning them as values when they will commonly be encountered.
  • Leave rare, exceptional cases implicit in your return type by raising exceptions when they occur.
  • When someone is not using your functions properly, immediately make them aware by raising an error.
5 Likes