Грешки


В Elixir, грешките са предназначени за ситуации, които никога не би трябвало да се случат при нормални обстоятелства. Това ще рече - ситуации при които зависим от нещо външно и то спре да работи, зависим от някакви файлове, а тях ги няма, като цяло когато имаме проблем с ресурсите и конфигурацията от които нашата програма зависи. Да речем грешен input от потребител не е такава ситуация.

‘Вдигане’ на грешка

Грешка се ‘вдига’ с raise:

raise "Ужаст!"
# (RuntimeError) Ужаст!

По подразбиране, ако не подадем тип на грешката на raise, тя е RuntimeError. Можем да вдигнем грешка и с тип, без съобщение:

raise RuntimeError
# (RuntimeError) runtime error

Както и с тип на грешката и съобщение:

raise ArgumentError, message: "Грешка, брато!"
# (ArgumentError) Грешка, брато!

Elixir идва с набор от грешки за различни случаи, които можете да видите в документацията под ‘EXCEPTIONS’.

Грешките в Elixir не са препоръчителни за употреба. Имат славата на GOTO/спагети програмиране и наистина е хубаво да помислим дали има нужда от тях в дадена ситуация.

Прието е функции, при които има проблем, да връщат {:error, <проблем>}, а ако се изпълняват с успех и имат резултат - {:ok, <резултат>}. Имената на функции, които биха могли да ‘вдигнат’ грешка, обикновено завършват на !. Да речем ако имаме SomeModule.some_function/1, която връща {:ok, result} или {:error, reason}, ако искаме да дефинираме същата, но при успех връщаща result, а при неуспех, ‘вдигаща’ грешка, ще я кръстим SomeModule.some_function!/1. Когато говорим за вход-изход ще видим, че функциите идващи с езика следват тази конвенция.

Прихващане на грешка

Можем да прихванем грешка, използвайки try/rescue блок:

try do
  1 / 0
rescue
  [RuntimeError, ArgumentError] ->
    IO.puts("Няма да стигнем до тук.")
  error in [ArithmeticError] ->
    IO.puts("На нула не се дели, #{error.message}")
  any_other_error ->
    IO.puts("Лошаво... #{any_other_error.message}")
else
  IO.puts("Няма грешка.")
after
  IO.puts("Finally!")
end

Примерът по горе показва няколко вида прихващане. Прихващането пък си е match-ване. Първият пример е чрез списък от тип грешки, докато във втория, виждаме как от този списък да си вземем грешката в променлива. Накрая хващаме всички типове, които не сме описали досега в променливата any_other_error.

Имаме и after клауза, която винаги ще се изпълни след като try функцията завърши, няма значение, дали е имало грешка или не. Другата интересна клауза в примера е else - тялото ѝ се изпълнява само ако не е възникнала грешка в тялото на try.

Създаване на нови типове грешки

Можем да създадем и нови типове грешки. Подобно на структурите, нова грешка се дефинира като част от модул:

defmodule VeryBadError do
  defexception message: "Лошо!!!"
end

Сега можем да я ‘вдигнем’:

try do
  raise VeryBadError
rescue
  error in VeryBadError ->
    IO.puts(inspect(error, structs: false))
end
# %{__exception__: true, __struct__: VeryBadError, message: "Лошо!!!"}

Както виждате, грешките са структури с още едно тайно поле - __exception__. То има стойност true. Нищо особено.

Throw/Catch

Тези две конструкции НЕ ТРЯБВА да се ползват. Има библиотеки, които поради някакво стечение на обстоятелствата е възможно да ги ползват, но ги избягвайте. Ние ви ги показваме за да ви кажем - не ги ползвайте.

С throw ‘подхвърляме’ стойност, която може да се ‘хване’ по-късно:

try do
  throw 5
catch
  x -> IO.puts(x)
end
# 5

Показахме ви ги. Сега - забравете за тях и не ги ползвайте.

Още няколко думи

Тази статия е доста кратка. Идеята ѝ е да ни запознае с грешките в elixir и да ни каже - ‘не ги ползвайте’. Два факта:

  1. В кода на mix няма прихващане на грешки.
  2. В кода на компилатора на Elixir има точно пет прихващания. Но все пак това е компилатор и там се случват доста магически неща.

Ако започнете да създавате нов тип за грешки се замислете.

В Erlang/Elixir кодът върви в специални изолирани процеси. Идеологията е - остави го да се счупи. Това е така защото тези процеси са наистина изолирани и не споделят състояние. Ако един падне, друг ще бъде вдигнат на негово място и така ако е имало проблем, възникнал runtime, той ще се изчисти. Няма защо ние да правим това. Ще си говорим за тези процеси в следващи статии, засега е важно да запомните, че писането на нови типове грешки е нещо, което Elixir програмистите като цяло НЕ правят. Същото се отнася и за ‘прихващането’ им. И без никакви throw/catch-ове!