Spec-Zone.ru › Elixir 1.16

Исходный код Обработка исключений (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"}

В примере выше обрабатывается исключение runtime и возвращается само исключение, которое затем выводится в сессии 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"

Использование try/catch уже нечасто, а использование его для перехвата завершений — ещё реже.

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

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

After

Иногда необходимо гарантировать очистку ресурса после действия, которое потенциально может вызвать ошибку. Конструкция 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 указан.

Else

Если блок 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.32.2) для программного языка Elixir

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

Spec-Zone.ru

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