In a with case, with many operations, how can i individually handle errors?
Match on the reason
. If though all of them simply return the same reason, you can only wrap your stuff in extra tuples, eg (but untested as on mobile):
with {:fetch_data, {:ok, data}} <- {:fetch_data, fetch_data(stuff)} do
data
else
{:fetch_data, :error} -> {:error, :fetch_data_failed}
end
Damm, i was thinking in this exactly solution, but it just seems so verbose. Take this example:
with {:parse_limit, {:ok, limit}} <- {:parse_limit, parse_limit(request)},
{:parse_offset, {:ok, offset}} <- {:parse_offset, parse_offset(request)},
{:parse_filters, {:ok, filters}} <- {:parse_filters, parse_filters(request)} do
# send the content back to user
else
{:parse_limit, :error} -> ...
{:parse_offset, :error} -> ...
{:parse_filters, :error} -> ...
end
Itâs seems very verbose, and other thing, how can i handle the middle pre values? Imagine that the error happens on parse_filters, from the parse_filters error, how can i access the limit and offset?
By adding them to the tuple.
I would suggest for you change each individual parse_
function to already return the error in the format you desire. Your code should look like this:
with {:ok, limit} <- parse_limit(request),
{:ok, offset} <- parse_offset(request, offset),
{:ok, filters} <- parse_filters(request, limit, offset) do
# send the content back
else
{:error, message} -> # send error message back to the user
end
If each parse function already returns the error in the format desired, it gets much simpler, as you no longer need to tag everything just to untag it right after.
But the thing is, maybe, each error can have a individual treatment. Parsing limit, offset and filters, always will return a 4XX HTTP error to the client, as the request is invalid. A repository error, on the other side, will return a 5XX.
In this case, the only way is to tag the errors like @NobbZ was talking about, right?
Scott Wlaschin: Use Functions!
...
with {:ok, limit} <- parse_limit(request),
{:ok, offset} <- parse_offset(request, limit),
{:ok, filters} <- parse_filters(request, limit, offset) do
# send the content back
else
error -> handle_parse_error(error)
end
end
def handle_parse_error({:error, {:parse_limit, message}}) do
# handle parse_limit error
...
end
def handle_parse_error({:error, {:parse_offset, message, limit}}) do
# handle parse_offset error
...
end
def handle_parse_error({:error, {:parse_filters, message, limit, offset}}) do
# handle parse_filters error
...
end
Yep, this is it, you nailed it. Iâm still grasping with Elixir, but this looks very good.
A+ for that talk link
A further step you can take is to create an error struct, which might implement the Exception
behavior.
One big benefit of using structs over tuples for the error is you can add or remove data from a struct easier than you can a tuple (it wonât change the shape for pattern matching).
I generally tend towards creating a generic struct type for my app that looks like:
%YourApp.Error{type: :x, reason: :y, data: %{...}}
Using this, I would create instances of the error struct when parsing and @peerreyndersâs handle error function might then look like this:
def handle_parse_error({:error, %YourApp.Error{type: :parse_limit} = error}) do
do_some_thing_with(error.data.message)
end
def handle_parse_error({:error, %YourApp.Error{type: :parse_offset} = error}) do
do_something_with(error.data.message, error.data.offset)
end
A further refinement Iâve been working on, particularly for Phoenix apps, is to limit the number of types for those generic errors. For example, the parse errors might be:
%YourApp.Error{
type: :invalid_input,
reason: :could_not_parse_limit,
data: %{message: "..."}
}
%YourApp.Error{
type: :invalid_input,
reason: :could_not_parse_offset,
data: %{message: "...", limit: ...}
}
%YourApp.Error{
type: :invalid_input,
reason: :could_not_parse_filters,
data: %{message: "...", offset: ..., limit: ...}
}
Then you can define handlers for these generic errors in a action_fallback
controller, in this case to render a 400 response for :invalid_input
errors. Then your with
clause in a controller just looks like:
with {:ok, limit} <- parse_limit(request),
{:ok, offset} <- parse_offset(request, limit),
{:ok, filters} <- parse_filters(request, limit, offset) do
# send the content back
end # No need to handle errors, let them fall through and the action_fallback will render
If it is decoupling youâre after, the reason
in {:error, reason}
should be entirely opaque to the client of the module (which issued the error) - i.e.:
- only the module that issued the error tuple should know the actual structure of
reason
- and the module should offer functions that allow the client to intelligently interrogate the relevant aspects of
reason
. For example:
MyModule.error_message(reason)
could retrieve the core message of the error regardless whether reason
is just a plain string or whether it needs to be assembled from various labyrinthically nested maps that are part of reason
.
Similarly the module can offer higher level assessments like MyModule.error_timeout?(reason)
or MyModule.error_temporary?(reason)
while remaining in complete control of the actual structure and content of the reason
information - never disclosing any details to the client other than through the moduleâs reason
interrogation functions.
That is a perfectly valid (and probably often good) approach, though regardless of whether you react to the error itself or the return value of a function that interrogates the error you have a point of coupling.
I find using an error/exception struct in {:error, %Error{}}
tuples provides a good, flexible solution for representing errors that can be used simply in with
or an action_fallback
or any other error handling situation. Tuples or opaque objects or any similar concept could work just as well, I just presented what Iâve found to be a good solution.
I think the important part is that people should strive to return context rich {:error, reason}
tuples instead of just returning {:error, :not_found}
or even worse, just returning :error
.
Fair enough - though under particular circumstances coupling to functions can make more sense - example: protocols.
But this doesnât work, does it? handle_parse_error
is assuming those 3 functions return tuples that include their own function name in an inner tuple?
def parse_limit(request) do
..
{:error, {:parse_limit, :request_had_typo_or_something}}
end
Instead of the more usual (I thought?) {:error, :request_had_typo_or_something}
What doesnât work?
The problem with {:error, :reason_atom}
is that it doesnât have a lot of context. If youâre handling that error exactly at the point where the problem occurs then you probably have enough context, but if youâre dealing with the error somewhere else, such as a FallbackController
, then youâll generally want to make your errors richer.
For example, if youâre not at the point the error occurred the :error
returned by Map.fetch
is probably less useful than {:error, required_parameter_not_found}
, which is in turn less useful than {:error, {:required_parameter_not_found, :i_am_a_required_parameter}}
.
Another variation on the same theme (see previous post):
...
with {:parse_limit, {:ok, limit}} <- {:parse_limit, parse_limit(request)},
{:parse_offset, limit, {:ok, offset}} <- {:parse_offset, limit, parse_offset(request)},
{:parse_filters, limit, offset, {:ok, filters}} <- {:parse_filters, limit, offset, parse_filters(request)} do
# send the content back
else
error -> handle_parse_error(error)
end
end
def handle_parse_error({:parse_limit, {:error, message}}) do
# handle parse_limit error
...
end
def handle_parse_error({:parse_offset, limit, {:error, message}}) do
# handle parse_offset error
...
end
def handle_parse_error({:parse_filters, limit, offset, {:error, message}}) do
# handle parse_filters error
...
end