Исходный код 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 вопросе: написании документации.
© 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