39
votes

Background

Both try/rescue and try/catch are error handling techniques in Elixir. According the corresponding chapter in the introduction guide.

Errors can be rescued using the try/rescue construct

On the other hand,

throw and catch are reserved for situations where it is not possible to retrieve a value unless by using throw and catch.

Doubts

I have a brief understanding that rescue is for errors. While catch is for any value.

However,

  • When should I make use of the error handling mechanisms in Elixir?
  • What are the differences between them in detail?
  • How should I pick one to use in a specific use case?
  • What exactly are 'the situations where it is not possible to retrieve a value unless by using throw and catch'?
5
You shouldn't use them at all ;) It's not a Java that you want to try/catch all errors. You should use supervisors when error occured to make your app robust or use raise whenever error should occur.PatNowak
Besides the accepted answer, the table at the answer by Dimagog is great.xji

5 Answers

33
votes

It's a good question.After research a bit.

  • What is the differences between them in details?

    José's answer:

Mainly, you should use throw for control-flow and reserve raise for errors, which happens on developer mistakes or under exceptional circumstances.

In Elixir, this distinction is rather theoretical, but they matter in some languages like Ruby, where using errors/exceptions for control-flow is expensive because creating the exception object and backtrace is expensive.

  • How should I pick one to use in a specific use case?

Please check this answer Which situations require throw catch in Elixir

Shortly:

raise/rescue

Consider raise/rescue to be explicitly about exception handling (some unexpected situation like programmer errors, wrong environment, etc).

throw/catch

Is useful in places where you have expected failures. Classic examples are:

The last one:

  • What exactly are 'the situations where it is not possible to retrieve a value unless by using throw and catch'?

Let's say you are trying to running some code from a process that is supervised by a Supervisor but the process dies for an unexpected reason.

try do
IO.inspect MayRaiseGenServer.maybe_will_raise
rescue
  RuntimeError -> IO.puts "there was an error"
end

MayRaiseGenServer is supervised by a Supervisor and for some reason an error was raised:

try do
IO.inspect MayRaiseGenServer.maybe_will_raise # <- Code after this line is no longer executed

And then you can come up with using catch an exception here:

try do
  IO.inspect MayRaiseGenServer.maybe_will_raise
catch
  :exit, _ -> IO.puts "there was an error"
end

Ok.Hope that clarify enough what we are looking for.

26
votes

Other answers already cover usage of raise vs. throw well.

I'll describe the mechanics of how to handle each exceptional situation using a table:

creating | handling with  | where y is
-----------------------------------------------------
raise x  | rescue y       | %RuntimeError{message: x}
error(x) | rescue y       | %ErlangError{original: x}
throw x  | catch y        | x
exit(x)  | catch :exit, y | x

where error(x) is actually :erlang.error(x).

On top of this, both rescue and catch/1 (catch with 1 argument) are just a syntactic sugar. All 4 cases above could be handled with catch/2:

creating | handling with | where y is | and z is
-----------------------------------------------------------------
raise x  | catch y, z    | :error     | %RuntimeError{message: x}
error(x) | catch y, z    | :error     | x
throw x  | catch y, z    | :throw     | x
exit(x)  | catch y, z    | :exit      | x

Note the asymmetry of handling raise and error with rescue vs. catch/2: x is wrapped into %ErlangError when rescue is used, but not with catch/2.

2
votes

I like to use the analogy:

You either catch a thrown ball or rescue someone from a mountain.

  • catch - is expected and used for control flow (e.g. Java-like error handling)
  • rescue - for unexpected errors (e.g. runtime errors)
1
votes

By reading Dimagog's answer, and the article found at https://inquisitivedeveloper.com/lwm-elixir-48/, I really gained a lot of insight on the matter. I am just sharing a personal practical example,

chset = 
  %SomeModel{}
  |> SomeModel.changeset(attrs)
try do 
  chset
  |> Repo.insert()
catch :error,  %Postgrex.Error{postgres: %{code: :invalid_password}} ->
  { :error ,
    chset
    |> Changeset.add_error(:username, "may be invalid")
    |> Changeset.add_error(:password, "may be invalid")
  }
else    
  {:ok, lr} -> {:ok, Map.put(lr, :password, nil)}
  error -> error
end 

The postgresql error code comes from a plpgsql function in which I raise an error, as follows,

 raise invalid_password using
   message = 'Invalid username or password' , 
   detail = 'A user could not be found that matched the supplied username and password';
0
votes

try/catch has to be used when an error is known (like request validation errors) and raise/rescue has to be used to catch exceptions (exceptions are unknown or unhandled errors).

A combination of both of these can be used. For example,

def example(conn, params) do 
    try do
      data  = %{}
      types = %{field1: :string, field2: :integer}
  
      changeset =
        {data, types}
        |> cast(params, [:field1, :field2])
        |> validate_required([:field1, :field2])

      if (!changeset.valid?) do
        throw({:ClientError, changeset.errors })
      end
      
      # some logic
      
      raise ArgumentError
    catch
      # client error is caught here
      {:ClientError, error} ->
        IO.inspect(error, label: "client error")
    rescue
      exception ->
        # Argument error is caught here
        IO.inspect(exception, label: "exception") 
    end
  end