Have you read the official guides? You get a feel for how to handle expected errors via return tuples/status codes. And then there is the section on try/catch/rescue which specifically talks about exceptions and even touches on “let it crash”. Perhaps there could be a named section on dealing with expected errors, but I wouldn’t say this information isn’t available in official guides.
That’s cool, I’ve never considered that before.
It depends on how structured you want it. Use defstruct
for domain entities for sure (ecto returns structs). You general want a struct when you have specific behaviour associated with them which you define in the same module (which makes it feel a little like OO without the state encapsulation). I actually don’t have a great example of when to use bare maps but maybe someone else does? If I don’t care about well structured data, I just simple tuples. I’m also on my phone right now so it’s annoying to type out code
I think you have not fully understood what I was looking for.
Yes I’ve read the guide and I know the Let It Crash philosophy.
In The guide, it talks about something completely different than what we were discussing above. I know about try/catch/rescue, just wanted to get some opinions about how to represent errors in a scalable manner and a standard way(currently suggestion is to use {:error, exeception}
tuples)
That’s fair. My bad!
method signatures
You have been banned from ElixirForum. Hehe, jk.
Function returns are tricky, especially when using with
imo. I usually end up making wrapper functions…
defmodule Foo do
def call(config) do
with {:ok, var} <- System.fetch_env("FOO"),
{:ok, blah} <- Keyword.fetch(config, "BLAH")
do
do_something_with(var, blah)
else
:error -> "htf do I know which one failed??"
end
end
end
With wrappers…
defmodule Foo do
def call(config) do
with {:ok, var} <- fetch_env_foo()
{:ok, blah} <- fetch_config_blah()
do
do_something_with(var, blah)
else
{:error, :fetch_env_foo} -> handle_fetch_env_foo_error()
{:error, :fetch_config_blah} -> handle_fetch_config_blah_error()
end
end
defp fetch_env_foo do
case System.fetch_env("FOO") do
:error -> {:error, :fetch_env_foo}
ok -> ok
end
end
defp fetch_config_blah do
case Keyword.fetch(config, "BLAH") do
:error -> {:error, :fetch_config_blah}
ok -> ok
end
end
end
Curious what other people think about this.
Aren’t you missing : there?
I might do it this way, just so I need just one branch to handle all missing envvars. I’m not that happy that it’s just plain tuple, map could be better, not sure about that…
:error -> {:error, {:fetch_env, "FOO"}}
If the errors matter and I’m handling them in the function, I use nested case
statements. If I don’t care about the error I use a with
. If I want to return an error sometimes I’ll return an exception struct.
Not a fan of tagged tuples. Put them in the bin
What are you matching on with your case
s then?
Sorry, that was a bit vague. I am not a fan of creating tagged tuples in a with
statement for the sole purpose of matching on them in the else
clause.
I’m all for ok/error tuples
Tagged tuples? Is that another name for a map?
Strangely no one has mentioned structures yet? Are you guys not using them? Is that too much work for example?
No. Since the previous case handles errors, I’m assuming the “fall through” or “catch all” case means success. ok
is a var that matches anything and just returns it.
x = case {:ok, "foo"} do
:error -> {:error, "no good"}
ok -> ok
end
In that case, x
will be {:ok, "foo"}
.
I only use structs when the error can possibly have tons of info, otherwise it’s just easier to match on {:error, atom}
.
For example, I have a clustered Memcached client that supports multiget, multiset, etc. That definitely returns {:error, %Error{...}}
because there is lots of info about what went wrong (connection error? which server? did some keys succeed? Which did, which didn’t?).
Off the top of my head HTTPoison and Xandra both do the whole {:error, %ComplexErrorStruct{...}}
thing. Works great and makes sense since the errors contain lots of info.
Strangely no one has mentioned structures yet?
I can’t think of any return-value-specific guidance on structs - in general, they’re the right choice when:
- you have a lot of separate pieces of data that would make a tuple unwieldy
- the users of that data depend on specific keys, so a type more specific than
map()
is useful
As @cjbottaro points out, HTTPoison is a good example of this; returning a giant tuple or a map from a function like request
wouldn’t be ideal.
Just as a curiosity maybe - the pattern I am occasionally following is:
{<return code>, <returned data>, <error/exception>}
, with instances of it being:
{:ok, data, nil}
{:err, nil, exception_or_error}
It was giving me sense of that, I don’t need to care much about types returned, and order of elements is everything, which is not really the case.
What’s the benefit of having one element of the tuple always be nil
?
It reminds me of Go’s result, err
idiom, but that uses err == nil
to signal :ok
.
I could maybe see if being useful if you had a mixed list of OK / error results and wanted to get all the data (and nil
on error) - you could spell it Enum.map(list_of_mixed_results, &elem(&1, 1))
…
ok -> ok
Ah, so it’s more like result → result, got you…
What’s the benefit of having one element of the tuple always be
nil
?
keeping length of a tuple same I guess
length of a tuple and order of elements is part of its API, data model etc. - you could say.
^^^ OK, that part makes little to no sense. Please disregard it
I get your point, it’s more of a curiosity than anything else - it would make sense only when both data and error can be returned at the same time.
Tagged tuples? Is that another name for a map?
{:ok, result}
and {:error, error}
are tagged tuples. The primary value of tagged tuples being for pattern matching, some people use them in with
statements just to match on multiple else
clauses, such as this example (of what not to do) from @keathley 's blog post Good and Bad Elixir
with {:service, {:ok, resp}} <- {:service, call_service(data)},
{:decode, {:ok, decoded}} <- {:decode, Jason.decode(resp)},
{:db, {:ok, result}} <- {:db, store_in_db(decoded)} do
:ok
else
{:service, {:error, error}} ->
# Do something with service error
{:decode, {:error, error}} ->
# Do something with json error
{:db, {:error, error}} ->
# Do something with db error
end
I think this is the (anti-)pattern @cmo was referring to, and something José Valim has also publicly discouraged.