verkhovin

verkhovin

How to handle error handling boilerplate

Hi guys!
Want to open here another discussion about error handling concepts in Elixir because it looks like I really stuck here by myself.

My question is about errors that are related to users interactions, where you can’t just “Fail fast and forget / Let it crash” but need to respond with some appropriate error message.
Elixir community’s standard is to return tuples like {:error, reason} when something goes wrong during function execution. Overall, it looks good at the first sight.
But at some point, I noticed that to handle this approach I have to write tons of case or with really often just to check that underlying call didn’t return this {:error, reason} tuple.

Consider an example of a multilayered application (like ControllerBusiness Logic → database CRUDs). If there is some expected error that occurs on the layer of CRUDs, I will need to have a case to catch this error on a Business Logic layer and pass it to Controller. After that, I need to have another case on the Controller’s layer. And it becomes even worse if you have more than 3 layers there, or your business logic includes something more than just a call to CRUD layer.
Maybe I really missed something but it seems to me that this approach makes you write a lot of boilerplate.

I’m actually from Java world and it would be quite natural for me to resolve this issue by throwing (raising) an exception from the CRUDs level and catch in on the Controller level. But if I understand correctly, exceptions in Elixir philosophy are more about unexpected errors that should be handled by this “Let it crash” way.

My question still is: is it normal and common way to write elixir code having this error checks (case/with) on every layer of the application?

I can provide some naive code example if my thoughts are not clear.

Most Liked

sezaru

sezaru

I’m not sure if I follow you when you say you cannot redirect them all to the controller.

Let’s say you have a function CRUD.get that returns {:ok, value} or :error, reason} and that reason can be multiple types of errors.

Now let’s say you want to redirect the errors to the controller from your business logic and handle the :ok case, if you redirect something to the controller via this function return, you can simply do something like:

with {:ok, value} <- CRUD.get() do
  # Do something with value
end

This will handle the :ok case and redirect all the other ones.

If you need to call a function to redirect it to the controller, you can do something like:

with {:ok, value} <- CRUD.get() do
  # Do something with value
else
  {:error, reason} -> Controller.redirect({:error, reason})
end

or

case  CRUD.get() do
 {:ok, value} ->
    # Do something with value

 {:error, reason} -> 
    Controller.redirect({:error, reason})
end

In all these cases you don’t need to have a case for each error tuple, you can simply pattern match all of them.

Now at the Controller, if you want to handle each one of the errors, then you will need to use case or function pattern match to handle it, but that would be the same with java handling the catch cases.

Doesn’t that solves the issue or am I missed the point entirely of what the real problem is in your case?

joaquinalcerro

joaquinalcerro

You can also check this interesting post from @michalmuskala - Error Handling in Elixir Libraries | Michał Muskała

hauleth

hauleth

I would say that it depends on the requirements and what kind of error is that:

  • if there is reasonable way to do fallback, for example fetching data failed, but we can return cached result or something like that, then using ok/error tuple is ok
  • if there is no reasonable way to fallback, for example there is no user in the DB for given ID, then I would throw and let Plug.Exception do it’s thing
soup

soup

How you’ve written it is how I would have probably done so also, but I can’t speak as a community luminary.

Related: Good and Bad Elixir which may give you a bit of peripheral insight at least.

Specifically “Don’t pipe results into the following function”, which you maybe could have used in surface logic, i.e:

def do_surface_logic() do
  DeepLogic.do_deep_logic()
  |> do_some_response_preparations()
end

def do_some_response_preparations({:ok, val}) ...
def do_some_response_preparations({:error, e}) ...

But as you say, it’s only really shuffling stuff around.

See also “Raise exceptions if you receive invalid data.” in the same article.

By it’s nature, the article has to be opinionated but it’s definitely worth reading once or twice, “know when to break the rules”, etc blah blah blah.

I think the existence of {:ok | :error} and the line “In practice, Elixir developers rarely use the try/rescue construct” in the getting started guide gives the impression that exceptions should be avoided in Elixir, but they exist to be used and is probably a few good blog posts waiting to be written re: navigating between them and tuples.

amnu3387

amnu3387

I personally am very inclined to force explicitness and use more verbosity to describe flows. When using with I normally resort to tagging the branches, but sometimes you do need to pass through the underlying returns from the failing branch. When writing API endpoints I’ve settled in just having a %Response{}/%Output{} struct with :errors, :notices, :payload fields that the frontend knows how to interpret - this approach requires you to do it almost from the beginning and be in control also of how the front-end interacts. It’s also not completely feasible for html pipelines, as you can have redirects and so on, but could probably be made so.

Controllers/channels call only into context functions, and these entry points in the contexts either return those payload structs, or they return normal results and I have a translation module/functions for the returns. Again, it’s more boilerplaty.

But the issue that I think deserves a bit more thought is not that raising exceptions in themselves is bad (outside of things that really require raising, like a db disconnect, or inaccessibility of a resource, or inside a loop/recursion that no longer makes sense, but usually there you use throw), when you look at the flow of a single entry point, it can usually look clearer and in a way less verbose. The problem comes when all your code base applies this style. Then it basically becomes a mine field because everything can raise somewhere and you’re left having to keep in mind every detail of all functions you’re calling and what those themselves are calling.

When I have too much time to think about this nonsense I imagine I would like to see a variation of with (a special form named weave or steps) that would look like this:

weave # notice the new line 
   [not_found: %User{} = user] <- Db.Repo.get(User, user_id),
   [not_authorized: true] <- Contexts.Authorization.is_authorized?(user, some_action),
   [changeset: {:ok, %User{} = new_user}] <- Contexts.Users.update(user, some_action)
do
   {:ok, user}
else
  [changeset: changeset] -> {:error, changeset}
  [{error, _}] -> {:error, error}
end

Perhaps it could even be that when the do block was ommitted it would pass the last value, without the keyword.

weave 
   [not_found: %User{} = user] <- Db.Repo.get(User, user_id),
   [not_authorized: true] <- Contexts.Authorization.is_authorized?(user, some_action),
   [changeset: {:ok, %User{} = new_user}] <- Contexts.Users.update(user, some_action)
else
  [changeset: changeset] -> {:error, changeset}
  [{error, _}] -> {:error, error}
end

This can be confusing the first time you encounter it (not_authorized: true) but afterwards it’s simple to understand.

Other option would be:

weave
  %User{} = user <- Db.Repo.get(User, user_id) -> :not_found,
  true <- Contexts.Authorization.is_authorized?(user, some_action) -> :not_authorized,
  {:ok, %User{} = new_user} <- Contexts.Users.update(user, some_action) -> :changeset
else
  [changeset: changeset] -> {:error, changeset}
  [{error, _}] -> {:error, error}
end

Where Next?

Popular in Questions Top

minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
Exadra37
Sometimes I want to check if the input into a function is not a blank string. My first approach: defmodule Example do def do_stuff(s...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New

Other popular topics Top

TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
273 38985 115
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
William
I would like to know that is there any online source for learning Phoenix Framework for building E-Commerce Store? Any advantage on build...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

We're in Beta

About us Mission Statement