verkhovin

verkhovin

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.

Showing Posts 1 to 10

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
kokolegorille

kokolegorille

What You don’t have in Java is pattern matching and functions with multiple heads.

The first condition can often be solved with a function pattern matching some kind of result. ok, error is quite common in FP.

But if You are sure the operation cannot fail, You can use the ! operator.

It’s a good practice to push DB related to the outer boundaries. It’s also possible to use onion architecture. Where the central part is purely functional, and does not care about details of the DB layer.

The let it crash means something different. In case there is a problem You cannot predict, You are covered by the supervisors.

joddm

joddm

I think business logic and CRUD operations are usually handled by Ecto Changesets or multiple function heads which give an error tuple when applicable. Then you can handle any errors in the controller with a case statement as you’ve said.

verkhovin

verkhovin OP

Yes, I kind of feel this the same way.
I saw opinions here on the forum that throwing exceptions is some kind of “dirtier” solution than returning ok/error tuples. Maybe I misunderstood it somehow

Thank you for the answer!

verkhovin

verkhovin OP

Thank you for your answer!
If I understand you correctly, your point is that I can replace case/with with functions with multiple heads.
I think it is still the same boilerplate but in another form. Maybe I am too dramatic about this boilerplate issue by the way :smiley: I was just wondering if it is ok in FP world to pattern match almost every function call result to determine erroneous responses and handle them. As you said, it is quite common approach, right?

verkhovin

verkhovin OP

Thank you for your answer!

Sometimes it is not the case just to map response from Ecto directly up to the controller.

For example, you can have a chain of calls of functions from different modules. And each of these calls returns this ok/error tuple. You can’t just pass them all to the controller and the only option here is to pattern match each of these responses with with expression. So you handle it on this level in this way and then return ok/error tuple to the controller. And in turn, in the controller, you need to do the similar pattern matching again.
Hope it is clear.

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?

verkhovin

verkhovin OP

Thank you for you detailed answer!
Yes, this is almost what I was talking about. I’ll provide an example to show what I meant.
Let’s say that CRUD operation is not the only thing I need to do during the request handling. And let’s say there are more than 2 layers in the application. So Controller calls a function from SurfaceLogic module. And this function calls another one from DeepLogic module. DeepLogic module works with some CRUDs and external APIs through clients.

#DeepLogic module
def do_deep_logic() do
  with {:ok, response} <- ExternalApiClient.get_info(), # can return  {:error, :api_500_error}
         {:ok, _} <- CRUD.insert() # can return {:error, changeset}
  do
      {:ok, do_some_logic(response)} 
  else
     {:error, %Changeset{ ... } = changest} -> {:error, :already_exists}
     {:error, reason} -> {:error, reason}
  end
end
# SurfaceLogic module
def do_surface_logic() do
  #pattern match on {:ok} and just pass all the errors to the upper level:
  with {:ok, value} <- DeepLogic.do_deep_logic() do
     {:ok, do_some_response_preparations(value)}
  end
end
#Controller
def handle_post_request(conn, _) do
  case SurfaceLogic.do_logic() do
    {:ok, result} -> render( ... ) # render success
    {:error, :already_exists} -> render (...) # render already exists
    {:error, :api_500_error} -> render ( ... ) # something wrong with external data provider
  end
end

So now I need to have case/with on every single layer here. (Yes, I still have action_fallback on controller level, but it will just move matching from this controller to Fallback Controller, so I’ll leave it here now to make it easier to understand).

My point here is that all these case/with statements look like boilerplate that you need to have everywhere (on every single function call, if this function returns :ok/:error). And I just try to understand if it is common practice (considered as ok) or not.

A more natural way to handle this issue for me would raise exceptions on the level of CRUD and ExternalApiClient and catch them on the controller level. In this case there wouldn’t be a need in case/with on the level of DeepLogic and SurfaceLogic. But it seems like this way is more uncommon in Elixir/FP community.

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.

verkhovin

verkhovin OP

Thank you for the link. The article seems really useful! Going to read through it a couple of times.

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

Yes, I think this is exactly what makes me so confused about it

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews