Do you want to pick up from where you left of?
Take me there

Tratamento de Erros

Embora seja mais comum o retorno da tupla {:error, reason}, o Elixir suporta exceções e nesta lição veremos como lidar com erros e os diferentes mecanismos disponíveis para nós.

Em geral, a convenção em Elixir é criar uma função (example/1) que retorna {:ok, result} e {:error, reason} e uma função separada (example!/1) que retorna o result desempacotado ou levanta um erro.

Esta lição irá focar na interação com o último

Convenções Gerais

No momento, a comunidade Elixir chegou a algumas convenções sobre o retorno de erros:

Geralmente lidamos com fluxo de erros padrão com Pattern Matching, mas nesta lição, estamos focando no segundo caso - nas exceções.

Muitas vezes, em APIs públicas, você também pode encontrar uma segunda versão da função com um ! (exemplo!/1) que retorna o resultado desempacotado(ex: retorna o resultado no lugar de retornar uma tupla) ou levanta um erro.

Tratamento de Erros

Antes de podermos lidar com os erros, precisamos criá-los e a maneira mais simples de fazer isso é com o raise/1:

iex> raise "Oh no!"
** (RuntimeError) Oh no!

Se queremos especificar o tipo e mensagem, precisamos usar raise/2:

iex> raise ArgumentError, message: "the argument value is invalid"
** (ArgumentError) the argument value is invalid

Se sabemos que um erro pode ocorrer, podemos lidar com isso usando try/rescue e pattern matching:

iex> try do
...>   raise "Oh no!"
...> rescue
...>   e in RuntimeError -> IO.puts("An error occurred: " <> e.message)
...> end
An error occurred: Oh no!
:ok

É possível combinar vários erros em um único rescue:

try do
  opts
  |> Keyword.fetch!(:source_file)
  |> File.read!()
rescue
  e in KeyError -> IO.puts("missing :source_file option")
  e in File.Error -> IO.puts("unable to read source file")
end

Depois

Às vezes pode ser necessário executar alguma ação depois do nosso try/rescue independentemente do erro. Para isso temos try/after. Se você estiver familiarizado com Ruby isso é semelhante ao begin/rescue/ensure ou em Java try/catch/finally:

iex> try do
...>   raise "Oh no!"
...> rescue
...>   e in RuntimeError -> IO.puts("An error occurred: " <> e.message)
...> after
...>   IO.puts "The end!"
...> end
An error occurred: Oh no!
The end!
:ok

Esta é geralmente mais usada com arquivos ou conexões que devem ser fechados:

{:ok, file} = File.open("example.json")

try do
  # Do hazardous work
after
  File.close(file)
end

Novos Erros

Enquanto o Elixir inclui uma série de tipos de erro nativos, como RuntimeError, nós mantemos a capacidade de criar o nosso próprio se precisamos de algo específico. Criar um novo erro é fácil com o defexception/1 macro que aceita convenientemente a opção :message para definir uma mensagem de erro padrão:

defmodule ExampleError do
  defexception message: "an example error has occurred"
end

Vamos tentar forçar a execução do nosso novo erro:

iex> try do
...>   raise ExampleError
...> rescue
...>   e in ExampleError -> e
...> end
%ExampleError{message: "an example error has occurred"}

Lançar

Outro mecanismo para trabalhar com erros no Elixir é o throw e o catch. Na prática, isso não ocorre frequentemente no código mais recente de Elixir. Mas, é importante conhecer e entendê-los mesmo assim.

A função throw/1 nos dá a capacidade para sair da execução com um valor específico que podemos catch (pegar) e usar:

iex> try do
...>   for x <- 0..10 do
...>     if x == 5, do: throw(x)
...>     IO.puts(x)
...>   end
...> catch
...>   x -> "Caught: #{x}"
...> end
0
1
2
3
4
"Caught: 5"

Como mencionado, throw/catch são bastante incomuns e normalmente existem como tapa-buracos quando as bibliotecas não fornecem APIs adequadas.

Saindo

O mecanismo de erro final que o Elixir fornece é o exit. Sinais de saída ocorrem sempre que um processo morre e são uma parte importante da tolerância a falhas do Elixir.

Para sair explicitamente podemos usar exit/1:

iex> spawn_link fn -> exit("oh no") end
** (EXIT from #PID<0.101.0>) evaluator process exited with reason: "oh no"

Embora seja possível pegar uma saída com try/catch fazê-lo é extremamente raro. Em quase todos os casos é vantajoso deixar o supervisor controlar a saída do processo:

iex> try do
...>   exit "oh no!"
...> catch
...>   :exit, _ -> "exit blocked"
...> end
"exit blocked"
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!