Бесплатный курс по Elixir. Зарегистрируйтесь для отслеживания прогресса →

Elixir: Обработка ошибок, исключения

В Elixir есть механизм исключений, однако используется он иначе, чем в классических языках программирования. Для начала ознакомимся с исключениями.

Исключения выбрасываются через raise:

raise "Boom!"
# => ** (RuntimeError) Boom!
# =>    iex:149: (file)

Исключения создаются через defexception:

defmodule MyError do
  defexception message: "boom!"
end

raise MyError
# => ** (MyError) boom!
# =>   iex:150: (file)

raise MyError, message: "different boom!"
# => ** (MyError) different boom!
# =>   iex:150: (file)

Исключения перехватываются через try и rescue:

try do
  raise "boom!"
rescue
  e in RuntimeError -> e
end
# => %RuntimeError{message: "boom!"}

try do
  raise "boom!"
rescue
  RuntimeError -> "oops"
end
# => "oops"

Как уже было сказано, в мире Elixir используется философия "пусть сломается" (let it crash), из-за чего исключения при написании Elixir кода зачастую не обрабатываются. Почему? Из-за того, что Elixir код всегда исполняется в отдельных процессах (акторах), которые работают независимо. Поэтому падение одного из сотни или тысячи процессов никак не повлияет на работу всей системы. Про процессы (акторы) подробнее поговорим в следующем модуле, а пока продолжим изучать исключения.

Так как Elixir программисты зачастую игнорируют прямую обработку исключений, иногда бывает полезно поймать исключение, записать в логи информацию об ошибке и пробросить исключение дальше по стеку. Повторный выброс исключения происходит через reraise:

require Logger

try do
  1 / 0
rescue
  e ->
    Logger.warning(Exception.format(:error, e, __STACKTRACE__))
    reraise e, __STACKTRACE__
end

 # => ** (ArithmeticError) bad argument in arithmetic expression: 1 / 0
 # =>   :erlang./(1, 0)
 # =>   iex:157: (file)
 # =>   iex:161: (file)

 # => 17:48:09.214 [warning] ** (ArithmeticError) bad argument in arithmetic expression
 # =>   :erlang./(1, 0)
 # =>   iex:157: (file)
 # =>   (elixir 1.15.0) src/elixir.erl:374: anonymous fn/4 in :elixir.eval_external_handler/1
 # =>   (stdlib 4.3.1.1) erl_eval.erl:748: :erl_eval.do_apply/7
 # =>   (stdlib 4.3.1.1) erl_eval.erl:987: :erl_eval.try_clauses/10
 # =>   (elixir 1.15.0) src/elixir.erl:359: :elixir.eval_forms/4
 # =>   (elixir 1.15.0) lib/module/parallel_checker.ex:112: Module.ParallelChecker.verify/1
 # =>   (iex 1.15.0) lib/iex/evaluator.ex:331: IEx.Evaluator.eval_and_inspect/3

По сути, исключения в Elixir используются по прямому назначению, они выбрасываются только когда произошло действительно что-то, чего в нормальной ситуации произойти не должно. В большинстве же остальных языков программирования исключения используются как еще один способ управлять потоком выполнения программы. Почти оператор GOTO, только замаскированный.

Иногда нужно подчистить какой-то ресурс, при возникновении исключения, тогда подойдет after:

{:ok, file} = File.open("sample", [:utf8, :write])

try do
  IO.write(file, "hello")
  raise "oops"
after
  File.close(file)
end

Если же исключения не возникло, но нужно выполнить еще какой-то код, тогда подойдет else:

x = 2

try do
  1 / x
rescue
  ArithmeticError ->
  :infinity
else
  y when y < 1 and y > -1 -> :small
  _ -> :large
end
# => :small

Задание

Создайте функцию my_div, которая выбрасывает исключение ArgumentError с сообщением Divide x by zero is prohibited!, где x - первый переданный аргумент. Для деления с округлением воспользуйтесь Integer.floor_div:

Solution.my_div(128, 2)
# => 64

Solution.my_div(128, 0)
# => ** (ArgumentError) Divide 128 by zero is prohibited!
# =>  iex:142: Solution.my_div/2
# =>  iex:149: (file)

Solution.my_div(10, 0)
# => ** (ArgumentError) Divide 10 by zero is prohibited!
# =>  iex:142: Solution.my_div/2
# =>  iex:149: (file)
Упражнение не проходит проверку — что делать? 😶

Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:

  • Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет 🤨

Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.

Мой код отличается от решения учителя 🤔

Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.

В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.

Прочитал урок — ничего не понятно 🙄

Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.

Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.

Полезное


Нашли ошибку? Есть что добавить? Пулреквесты приветствуются https://github.com/hexlet-basics
Если вы столкнулись с трудностями и не знаете, что делать, задайте вопрос в нашем большом и дружном сообществе