Spec-Zone.ru › Elixir 1.18

Исходный код try, catch и rescue

В Elixir есть три механизма обработки ошибок: ошибки, броски и выходы. В этой главе мы рассмотрим каждый из них и укажем, когда следует использовать тот или иной механизм.

Ошибки

Ошибки (или исключения) используются, когда в коде происходят исключительные события. Образец ошибки можно получить, пытаясь добавить число к атому:

iex> :foo + 1
** (ArithmeticError) bad argument in arithmetic expression
     :erlang.+(:foo, 1)

Ошибка во время выполнения может быть поднята в любое время с помощью raise/1:

iex> raise "oops"
** (RuntimeError) oops

Другие ошибки могут быть подняты с помощью raise/2, передав имя ошибки и список ключевых аргументов:

iex> raise ArgumentError, message: "invalid argument foo"
** (ArgumentError) invalid argument foo

Вы также можете определить свои собственные ошибки, создав модуль и используя конструкцию defexception/1 внутри него. Таким образом, вы создадите ошибку с тем же именем, что и модуль, в котором она определена. Наиболее распространённый случай — определение пользовательского исключения с полем сообщения:

iex> defmodule MyError do
iex>   defexception message: "default message"
iex> end
iex> raise MyError
** (MyError) default message
iex> raise MyError, message: "custom message"
** (MyError) custom message

Ошибки могут быть перехвачены с помощью конструкции try/rescue:

iex> try do
...>   raise "oops"
...> rescue
...>   e in RuntimeError -> e
...> end
%RuntimeError{message: "oops"}

В примере выше перехватывается ошибка во время выполнения, и возвращается само исключение, которое затем выводится в iex сеансе.

Если вам не нужно использовать исключение, вы не должны передавать переменную в rescue:

iex> try do
...>   raise "oops"
...> rescue
...>   RuntimeError -> "Error!"
...> end
"Error!"

На практике разработчики Elixir редко используют конструкцию try/rescue. Например, во многих языках вы должны перехватить ошибку, когда файл не может быть успешно открыт. Elixir вместо этого предоставляет функцию File.read/1, которая возвращает кортеж, содержащий информацию о том, был ли файл успешно открыт:

iex> File.read("hello")
{:error, :enoent}
iex> File.write("hello", "world")
:ok
iex> File.read("hello")
{:ok, "world"}

Здесь нет try/rescue. Если вы хотите обработать несколько результатов открытия файла, вы можете использовать сопоставление с образцом с помощью конструкции case:

iex> case File.read("hello") do
...>   {:ok, body} -> IO.puts("Success: #{body}")
...>   {:error, reason} -> IO.puts("Error: #{reason}")
...> end

В тех случаях, когда вы ожидаете, что файл существует (и отсутствие файла действительно является ошибкой), вы можете использовать File.read!/1:

iex> File.read!("unknown")
** (File.Error) could not read file "unknown": no such file or directory
    (elixir) lib/file.ex:272: File.read!/1

В конечном счёте, ваше приложение должно решить, является ли ошибка при открытии файла исключительной или нет. Вот почему Elixir не навязывает исключения для File.read/1 и многих других функций. Вместо этого он оставляет разработчику выбор лучшего способа действий.

Многие функции в стандартной библиотеке следуют шаблону, имея пару, которая вызывает исключение вместо возвращения кортежей для сопоставления. Конвенция заключается в том, чтобы создать функцию (foo), которая возвращает кортежи {:ok, result} или {:error, reason}, и другую функцию (foo!, с тем же именем, но с последующим !), которая принимает те же аргументы, что и foo, но которая вызывает исключение, если возникла ошибка. foo! должна возвращать результат (не заключённый в кортеж), если всё прошло хорошо. Модуль File является хорошим примером этой конвенции.

Быстрая ошибка / Пусть рухнет

В сообществе Erlang, а также в Elixir, часто используется выражение «быстрая ошибка» / «пусть рухнет». Идея «пусть рухнет» заключается в том, что, если произойдёт что-то неожиданное, лучше всего позволить произойти исключению, не перехватывая его.

Важно подчеркнуть слово неожиданное. Например, представьте, что вы создаёте скрипт для обработки файлов. Ваш скрипт получает имена файлов в качестве входных данных. Ожидается, что пользователи могут ошибиться и предоставить неизвестные имена файлов. В этом случае, хотя вы можете использовать File.read!/1 для чтения файлов и позволить ему рухнуть в случае неверного имени файла, вероятно, имеет больше смысла использовать File.read/1 и предоставить пользователям вашего скрипта чёткое и точное сообщение об ошибке.

В других случаях вы можете ожидать, что определённый файл существует, и если его нет, это означает, что где-то произошла серьёзная ошибка. В таких случаях File.read!/1 — всё, что вам нужно.

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

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

Переброс

Хотя мы в целом стараемся избегать использования try/rescue в Elixir, одна ситуация, в которой мы можем захотеть использовать такие конструкции, — это наблюдаемость/мониторинг. Представьте, что вы хотите записать то, что пошло не так, вы можете сделать так:

try do
  ... some code ...
rescue
  e ->
    Logger.error(Exception.format(:error, e, __STACKTRACE__))
    reraise e, __STACKTRACE__
end

В примере выше мы перехватили исключение, записали его и затем перебросили его. Мы используем конструкцию __STACKTRACE__ и при форматировании исключения, и при его перебросе. Это гарантирует, что мы перебросим исключение как есть, без изменения значения или его источника.

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

Броски

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

Такие ситуации встречаются довольно редко на практике, за исключением случаев взаимодействия с библиотеками, которые не предоставляют надлежащий API. Например, предположим, что модуль Enum не предоставлял никакого API для поиска значения, и нам нужно было найти первое кратное 13 в списке чисел:

iex> try do
...>   Enum.each(-50..50, fn x ->
...>     if rem(x, 13) == 0, do: throw(x)
...>   end)
...>   "Got nothing"
...> catch
...>   x -> "Got #{x}"
...> end
"Got -39"

Поскольку модуль Enum предоставляет надлежащий API, на практике Enum.find/2 — правильный подход:

iex> Enum.find(-50..50, &(rem(&1, 13) == 0))
-39

Выходы

Весь код Elixir выполняется внутри процессов, которые общаются друг с другом. Когда процесс умирает «естественным путём» (например, из-за необработанных исключений), он отправляет сигнал exit. Процесс также может умереть, явно отправив сигнал exit:

iex> spawn_link(fn -> exit(1) end)
** (EXIT from #PID<0.56.0>) shell process exited with reason: 1

В примере выше связанный процесс умер, отправив сигнал exit со значением 1. Оболочка Elixir автоматически обрабатывает эти сообщения и выводит их на терминал.

exit также можно «перехватить» с помощью try/catch:

iex> try do
...>   exit("I am exiting")
...> catch
...>   :exit, _ -> "not really"
...> end
"not really"

catch также можно использовать в теле функции без соответствующего try.

defmodule Example do
  def matched_catch do
    exit(:timeout)
  catch
    :exit, :timeout ->
      {:error, :timeout}
  end

  def mismatched_catch do
    exit(:timeout)
  catch
    # Since no clause matches, this catch will have no effect
    :exit, :explosion ->
      {:error, :explosion}
  end
end

Однако использование try/catch уже является редким явлением, а использование его для перехвата выходов — ещё реже.

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

Именно благодаря этой системе надзора конструкции типа try/catch и try/rescue так редки в Elixir. Вместо перехвата ошибки мы предпочитаем «быструю ошибку», так как древовидная структура надзора гарантирует, что наше приложение вернётся к известному начальному состоянию после ошибки.

После

Иногда необходимо убедиться, что ресурс очищается после некоторого действия, которое может потенциально вызвать ошибку. Конструкция try/after позволяет это сделать. Например, мы можем открыть файл и использовать блок after для его закрытия — даже если что-то пойдёт не так:

iex> {:ok, file} = File.open("sample", [:utf8, :write])
iex> try do
...>   IO.write(file, "olá")
...>   raise "oops, something went wrong"
...> after
...>   File.close(file)
...> end
** (RuntimeError) oops, something went wrong

Блок after будет выполняться независимо от того, удастся ли блок try. Однако, если связанный процесс завершается, этот процесс также завершится, и блок after не будет выполнен. Таким образом, after обеспечивает только мягкую гарантию. К счастью, файлы в Elixir также связаны с текущими процессами, и поэтому они всегда будут закрыты, если текущий процесс завершится ошибкой, независимо от блока after. То же самое справедливо и для других ресурсов, таких как таблицы ETS, сокеты, порты и т.д.

Иногда вы можете обернуть всё тело функции в конструкцию try, часто для гарантии выполнения некоторого кода после этого. В таких случаях Elixir позволяет вам опустить строку try:

iex> defmodule RunAfter do
...>   def without_even_trying do
...>     raise "oops"
...>   after
...>     IO.puts("cleaning up!")
...>   end
...> end
iex> RunAfter.without_even_trying
cleaning up!
** (RuntimeError) oops

Elixir автоматически обернёт тело функции в блок try, когда один из after, rescue или catch указан. Блок after обрабатывает побочные эффекты и не изменяет возвращаемое значение из указанных выше блоков.

Иначе

Если присутствует блок else, он будет соответствовать результатам блока try, когда блок try завершается без броска или ошибки.

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

Исключения в блоке else не перехватываются. Если ни одна конструкция внутри блока else не соответствует, будет вызвано исключение; это исключение не перехватывается текущим блоком try/catch/rescue/after.

Область видимости переменных

Аналогично case, cond, if и другим конструкциям в Elixir, переменные, определённые внутри блоков try/catch/rescue/after не выходят за пределы контекста. Другими словами, этот код недействителен:

iex> try do
...>   raise "fail"
...>   what_happened = :did_not_raise
...> rescue
...>   _ -> what_happened = :rescued
...> end
iex> what_happened
** (CompileError) undefined variable "what_happened"

Вместо этого вы должны вернуть значение выражения try:

iex> what_happened =
...>   try do
...>     raise "fail"
...>     :did_not_raise
...>   rescue
...>     _ -> :rescued
...>   end
iex> what_happened
:rescued

Кроме того, переменные, определённые в блоке do конструкции try недоступны внутри блока rescue/after/else. Это связано с тем, что блок try может завершиться в любой момент, а значит, переменные могут никогда не быть связаны в первую очередь. Следовательно, и это также недействительно:

iex> try do
...>   raise "fail"
...>   another_what_happened = :did_not_raise
...> rescue
...>   _ -> another_what_happened
...> end
** (CompileError) undefined variable "another_what_happened"

На этом заканчивается наше введение по try, catch и rescue. Вы обнаружите, что они используются реже в Elixir, чем в других языках. Далее мы поговорим о очень важном для разработчиков Elixir вопросе: написании документации.

← Предыдущая страница Символы
Следующая страница → Написание документации

Скачать версию ePub

Создано с помощью ExDoc (v0.36.1) для программного языка Elixir

© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.18.1/try-catch-and-rescue.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API