What are your recommendations on method signatures and return values?

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 :sweat_smile:

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 :put_litter_in_its_place:

What are you matching on with your cases 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 :relaxed:

1 Like

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"}.

1 Like

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.

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))

2 Likes

Ah, so it’s more like result → result, got you…

keeping length of a tuple same I guess :slight_smile:
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 :upside_down_face:

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.

{: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.

1 Like