Obsługa błędów

Choć bardziej popularnym rozwiązaniem jest zwracanie krotki {:error, reason}, Elixir wspiera też wyjątki — i w tej lekcji przyjrzymy się sposobom obsługi błędów i różnym dostępnym mechanizmom do tego służącym.

Ogólnie przyjętą w Elixirze zasadą jest tworzenie funkcji (example/1) zwracającej {:ok, result} albo {:error, reason} oraz oddzielnej funkcji (example!/1), która zwraca bezpośrednio result albo wyrzuca wyjątek.

W tej lekcji skupimy się na tej ostatniej opcji.

Ogólne konwencje

W tej chwili społeczność Elixira doszła do kilku konwencji dotyczących zwracania błędów:

Zazwyczaj radzimy sobie ze standardowymi błędami za pomocą dopasowania wzorców, ale w tej lekcji skupimy się na drugim przypadku — wyjątkach.

W publicznych API często można znaleźć drugą wersję funkcji z wykrzyknikiem w nazwie (example!/1), która zwraca nieopakowany w żadne dodatkowe struktury wynik albo wyrzuca wyjątek.

Obsługa błędów

Zanim obsłużymy błąd, musimy go wywołać — a najprostsztym sposobem, by to uczynić, jest użycie raise/1:

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

Jeżeli chcemy określić konkretny typ i komunikat błędu, powinniśmy użyć raise/2:

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

Jeśli wiemy, że może pojawić się błąd, to możemy go obsłużyć wykorzystując try/rescue i dopasowanie wzorców:

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

Możemy obsłużyć wiele wyjątków w pojedynczym bloku 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

Blok after

Czasami musimy podjąć pewne dodatkowe działania po wykonaniu kodu w bloku try/rescue, niezależnie od tego, czy błąd się pojawił, czy też nie. Służy do tego konstrukcja try/after. Odpowiada ona begin/rescue/ensure w Rubym czy try/catch/finally w Javie:

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

Najczęstszym przypadkiem użycia after jest zamykanie połączeń i plików:

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

try do
  # Wykonuj niebezpieczną pracę
after
  File.close(file)
end

Własne błędy

Elixir zawiera wiele wbudowanych typów błędów, takich jak na przykład RuntimeError, niemniej czasami zachodzi potrzeba stworzenia nowego typu, specyficznego dla naszego projektu. Stworzenie nowego błędu polega na wykorzystaniu makra defexception/1, które przyjmuje opcję :message, umożliwiającą nam ustawienie domyślnego komunikatu błędu:

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

Teraz sprawdźmy, jak sprawuje się nasz błąd:

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

Zwracanie błędów

Innym mechanizmem związanym z błędami w Elixirze jest użycie throw i catch. Nie występuje on za często, szczególnie w nowszym elixirowym kodzie, ale ważne jest, by o nim wiedzieć i rozumieć jego działanie.

Funkcja throw/1 pozwala na przerwanie wykonania kodu i przekazanie do catch pewnej wartości:

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"

Jak już wspomnieliśmy, throw/catch jest rzadko używanym mechanizmem, który zazwyczaj pojawia się jako tymczasowe rozwiązanie tam, gdzie biblioteki nie zapewniają odpowiedniego API .

Kończenie procesu

Ostatnim mechanizmem związanym z obsługą błędów dostarczanym nam przez Elixira jest exit. Sygnały wyjścia (exit) wysyłane są za każdym razem, kiedy kończy się proces i są bardzo ważną częścią odporności Elixira na błędy.

Możemy też jawnie wywołać exit/1:

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

Możliwe jest użycie try/catch do obsłużenia sygnału exit, ale takie rozwiązanie jest wyjątkowo rzadkie. W niemal wszystkich przypadkach najlepiej będzie, jeśli pozwolimy, by obsługą takiego zdarzenia zajął się nadzorca procesów (ang. supervisor):

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!