title | next_page | prev_page |
---|---|---|
Конструкция `try`, `catch` и `rescue` |
typespecs-and-behaviours |
sigils |
В Эликсире есть три механизма работы с непредвиденным поведением: ошибки, выбрасывания и выход. В этой главе мы рассмотрим каждый из них и случаи, когда использовать те или иные механизмы.
Ошибки (или исключения) используются, когда в коде происходят исключительные ситуации. Пример ошибки можно увидеть при попытке добавить число к атому:
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
внутри него. Таким образом вы создадите ошибку с тем же именем, что и у модуля, в котором она объявлена. Наиболее распространенный случай – объявление исключений с полем message
:
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
.
Если у вас нет причины использовать саму ошибку, не обязательно её предоставлять:
iex> try do
...> raise "oops"
...> rescue
...> RuntimeError -> "Error!"
...> end
"Error!"
На практике, однако, Эликсир-разработчики редко используют конструкцию try/rescue
. Например, многие языки обязали бы вас отловить ошибку, когда файл не может быть открыт. Эликсир же предоставляет функцию 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
и многих других функциях. Напротив, разработчик сам волен выбирать лучший способ поведения.
Для случаев, когда вы ожидаете, что файл существует (и отсутствие этого файла действительно ошибка), вы можете использовать File.read!/1
:
iex> File.read! "unknown"
** (File.Error) could not read file unknown: no such file or directory
(elixir) lib/file.ex:305: File.read!/1
Многие функции в стандартной библиотеке следуют схеме наличия двух вариантов функции, одна из которых вызывает исключения, вместо возврата кортежа. Договорённость состоит в том, чтобы была функция (foo
), которая возвращает кортеж {:ok, result}
или {:error, reason}
, и другая функция (foo!
, такое же имя с !
в конце), которая принимает те же аргументы, что и foo
, но вызывает исключения в случае ошибки. foo!
должна возвращать результат (не обёрнутый кортежем), если всё прошло хорошо. Модуль File
– хороший пример следования этой договорённости.
В Эликсире мы избегаем использования try/rescue
, потому что не используем ошибки для контроля работы приложения. Мы понимаем ошибки буквально: они оставлены для неожиданных и исключительных ситуаций. В случае, если вам действительно нужно контролировать ход работы, следует использовать выбрасывания с помощью throw. О нём мы и поговорим.
В Эликсире, некоторое значение может быть выброшено и далее отловлено. Конструкции 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
Весь код на Эликсире работает внутри процессов, которые общаются между собой. Когда процесс умирает по «естественным причинам» (например, необработанное исключение), он отправляет сигнал exit
. Процесс также может умереть при вызове сигнала exit
вручную:
iex> spawn_link fn -> exit(1) end
#PID<0.56.0>
** (EXIT from #PID<0.56.0>) 1
В примере выше, связанный процесс умирает при отправке сигнала exit
со значением 1. Оболочка Эликсира автоматически обрабатывает такие сообщения и выводит их в терминал.
Сигнал exit
также можно «отловить», используя try/catch
:
iex> try do
...> exit "I am exiting"
...> catch
...> :exit, _ -> "not really"
...> end
"not really"
Использование try/catch
само по себе не распространено, и использование его для отлова выходов ещё более редкое.
Сигналы exit
– важная часть отказоустойчивости, предоставляемой виртуальной машиной Эрланга. Процессы обычно работают в деревьях супервизоров, которые тоже являются процессами, которые слушают сигналы exit
от наблюдаемых ими процессов. Как только приходит сигнал выхода, процесс перезапускается.
Именно эта система супервизоров делает конструкции вроде try/catch
и try/rescue
настолько непопулярными в Эликсире. Вместо попытки решить ошибку, мы лучше побыстрее закончим с проблемным местом и дерево супервизора будет гарантировать работу нашего приложения, откатив его к известному начальному состоянию после ошибки.
Иногда необходимо убедиться, что ресурс очищен после некоторых действий, которые могли быть потенциальной причиной ошибки. Конструкция 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
без ошибок или нет. Однако, обратите внимание, если связанный процесс существует, этот процесс отправит exit
и секция after
не будет выполнена. after
предоставляет лишь неполную гарантию. К счастью, файлы в Эликсире также связаны с текущим процессом и всегда будут закрыты, если текущий процесс завершился с ошибкой, независимо от секции after
. Вы можете обнаружить, что так работают и другие ресурсы: таблицы ETS, сокеты, порты и др.
Иногда вы можете захотеть обернуть всё тело функции в конструкцию try
, часто чтобы убедиться, что некоторый код наверняка выполнится после. В этом случае, Эликсир позволяет опустить строку 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
Эликсир автоматически обернёт тело функции в try
, если обнаружит after
, rescue
или catch
.
Если указан блок else
, он будет выполнен после блока 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
блоком.
Важно понимать, что переменные, определенные внутри try/catch/rescue/after
блока не влияют на внешний контекст. Это связано с тем, что блок try
может завершиться с ошибкой и некоторые переменные могут никогда не достичь своего объявления. Другими словами, следующий код не корректен:
iex> try do
...> raise "fail"
...> what_happened = :did_not_raise
...> rescue
...> _ -> what_happened = :rescued
...> end
iex> what_happened
** (RuntimeError) undefined function: what_happened/0
Вместо этого, вы можете сохранить значение выражения из try
:
iex> what_happened =
...> try do
...> raise "fail"
...> :did_not_raise
...> rescue
...> _ -> :rescued
...> end
iex> what_happened
:rescued
На этом заканчивается наше введение в try
, catch
и rescue
. Вы можете обнаружить, что в Эликсире они используются реже, чем в других языках, хотя они могут быть полезны в некоторых ситуациях, когда библиотека или некоторая часть кода играет «не по правилам».