Исходный код 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 выполняется внутри изолированных процессов, которые по умолчанию ничего не разделяют. Поэтому необработанное исключение в процессе никогда не приведёт к падению или повреждению состояния другого процесса. Это позволяет нам определять процессы-надзиратели, которые предназначены для наблюдения за тем, когда процесс завершается неожиданно, и запускают новый процесс на его месте.
В конечном счёте, «быстрая ошибка» / «пусть это рухнет» — это способ сказать, что когда происходит что-то неожиданное, лучше всего начать заново в новом процессе, недавно запущенном надзирателем, а не слепо пытаться перехватывать все возможные случаи ошибок без полного контекста того, когда и как они могут произойти.
Повторное поднятие
Хотя в Elixir мы обычно избегаем использования try/rescue , одна ситуация, где мы можем захотеть использовать такие конструкции, — это возможность наблюдения/мониторинга. Представьте, что вы хотите записать, что что-то пошло не так, вы можете сделать так:
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.
Иначе
Если присутствует блок 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.17.2/try-catch-and-rescue.html