Статьи

Как обрабатывать исключения в эликсире

Обработка исключений — отличная практика для любой методологии разработки программного обеспечения. Будь то разработка на основе тестов, гибкие спринты или хакерская сессия со старым добрым списком задач, мы все можем извлечь выгоду из того, что наши базы будут защищены надежным подходом к обработке дефектов.

Крайне важно обеспечить, чтобы об ошибках позаботились, при этом они были эстетически приятны и, конечно, не становились большой проблемой логически с загадочными сообщениями для конечного пользователя, чтобы попытаться найти смысл. Если вы сделаете это, вы, безусловно, на правильном пути, чтобы создать надежное, стабильное и липкое приложение, с которым пользователи любят работать и которые очень рекомендуют другим.

Идеально для нас, Elixir обеспечивает расширенную обработку исключений с помощью нескольких механизмов, таких как try/catch , throws и кортеж {:error, reason} .

Чтобы отобразить ошибку, используйте raise в интерактивной оболочке, чтобы получить первый вкус:

1
2
iex> raise «Oh noez!»
** (RuntimeError) Oh noez!

Мы также можем добавить тип к этому так:

1
2
iex> raise ArgumentError, message: «error message here…»
** (ArgumentError) error message here…

Некоторые способы исправления ошибок в Elixir на первый взгляд могут быть неочевидны.

  • Во-первых, о процессах — используя spawn , мы можем создавать независимые процессы. Это означает, что сбой в одном потоке не должен влиять на любой другой процесс, если только не было какой-либо связи. Но по умолчанию все останется стабильным.
  • Чтобы уведомить систему о сбое в одном из этих процессов, мы можем использовать макрос spawn_link . Это двунаправленная ссылка, которая означает, что, если связанный процесс завершается, сигнал выхода будет активирован.
  • Если сигнал выхода отличается от :normal , мы знаем, что у нас есть проблема. И если мы перехватим сигнал выхода с помощью Process.flag(:trap_exit, true) , сигнал выхода будет отправлен в почтовый ящик процесса, где можно будет разместить логику о том, как обрабатывать сообщение, что позволит избежать серьезного сбоя.
  • Наконец, у нас есть мониторы, которые похожи на spawn_links , но это однонаправленные ссылки, и мы можем создать их с Process.monitor .
  • Процесс, который вызывает Process.monitor , получит сообщения об ошибках при сбое.

Для примера ошибки попробуйте добавить число к атому, и вы получите следующее:

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

Чтобы конечный пользователь не получил ошибку, мы можем использовать методы try, catch и rescue, предоставляемые Elixir.

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

Использование try/rescue похоже на использование блока try/catch который вы, возможно, видели в других языках программирования. Давайте посмотрим на пример в действии:

1
2
3
4
5
6
7
iex> try do
…> raise «do failed!»
…> rescue
…> e in RuntimeError -> IO.puts(«Error: » <> e.message)
…> end
Error: do failed!
:ok

Здесь мы используем блок try/rescue и вышеупомянутый raise чтобы поймать RuntimeError .

Это означает, что ** (RuntimeError) умолчанию для ** (RuntimeError) умолчанию не отображается и заменяется на более качественный форматированный вывод из вызова IO.puts .

Рекомендуется использовать сообщение об ошибке, чтобы предоставить пользователю полезный вывод на простом английском языке, который поможет им в решении этой проблемы. Мы рассмотрим это подробнее в следующем примере.

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

1
2
3
4
5
6
7
8
try do
  opts
  |> Keyword.fetch!(:source_file)
  |> File.read!
rescue
  e in KeyError -> IO.puts «missing :source_file option»
  e in File.Error -> IO.puts «unable to read source file»
end

Здесь мы поймали две ошибки в rescue .

  1. Если файл не может быть прочитан.
  2. Если символ :source_file отсутствует.

Как упоминалось ранее, мы можем использовать это для создания понятных сообщений об ошибках для нашего конечного пользователя.

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

Как всегда, работая в Elixir, KISS — лучший подход.

Существуют ситуации, когда вам потребуется конкретное действие, выполненное после блока try / rescue, независимо от того, была ли какая-либо ошибка. Для разработчиков Java или PHP вы можете подумать о try/catch/finally или Ruby’s begin/rescue/ensure .

Давайте посмотрим на простой пример использования after .

01
02
03
04
05
06
07
08
09
10
iex> try do
…> raise «I wanna speak to the manager!»
…> rescue
…> e in RuntimeError -> IO.puts(«An error occurred: » <> e.message)
…> after
…> IO.puts «Regardless of what happens, I always turn up like a bad penny.»
…> end
An error occurred: I wanna speak to the manager!
Regardless of what happens, I always turn up like a bad penny.
:ok

Здесь вы видите after того, как используется для постоянного отображения сообщений (или это может быть любая функция, которую вы хотели бы добавить туда).

Более распространенная практика, которую вы найдете в этом, — это доступ к файлу, например, здесь:

1
2
3
4
5
6
7
{:ok, file} = File.open «would_defo_root.jpg»
try do
   # Try accessing file here
after
   # Ensure we clean up afterwards
   File.close(file)
end

Наряду с методами raise и try/catch которые мы описали ранее, у нас также есть макросы throw и catch.

Использование метода throw завершает выполнение с определенным значением, которое мы можем найти в нашем блоке catch и используем далее так:

01
02
03
04
05
06
07
08
09
10
11
12
iex> try do
…> for x <- 0..10 do
…> if x == 3, do: throw(x)
…> IO.puts(x)
…> end
…> catch
…> x -> «Caught: #{x}»
…> end
0
1
2
«Caught: 3»

Так что здесь у нас есть возможность catch все, что мы throw в блоке try. В этом случае условное условие if x == 3 является триггером для нашей команды do: throw(x) .

Вывод из итерации, полученной из цикла for, дает нам четкое понимание того, что произошло программно. Постепенно мы вышли вперед, и исполнение было остановлено на catch .

Из-за этой функциональности иногда бывает трудно представить, где будет реализована функция throw в вашем приложении. Одним из главных мест было бы использование библиотеки, где API не имеет адекватной функциональности для всех результатов, представляемых пользователю, и было бы достаточно уловки для быстрого перемещения по проблеме, вместо того, чтобы разрабатывать гораздо больше внутри библиотеки для обработки вопрос и вернуть соответственно для него.

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

Выходы сигнализируются так:

1
2
iex> spawn_link fn -> exit(«you are done son!») end
** (EXIT from #PID<0.101.0>) «you are done son!»

Сигналы выхода запускаются процессами по одной из следующих трех причин:

  1. Нормальный выход : это происходит, когда процесс завершил свою работу и завершил выполнение. Поскольку эти выходы абсолютно нормальны, обычно ничего не нужно делать, когда они происходят, очень похоже на exit(0) в C. Причиной выхода для этого вида выхода является атом :normal .
  2. Из-за необработанных ошибок : это происходит, когда внутри процесса возникает необработанное исключение, без блока try/catch/rescue или throw/catch для его устранения.
  3. Принудительно уничтожено : это происходит, когда другой процесс посылает сигнал на выход с причиной :kill , которая принудительно завершает процесс получения.

В любой данный момент обновления при throw , exit или errors вызов System.stacktrace вернет последнее вхождение в текущем процессе.

Трассировка стека может быть немного отформатирована, но это может измениться в новых версиях Elixir. Для получения дополнительной информации об этом, пожалуйста, обратитесь к странице руководства .

Чтобы вернуть трассировку стека для текущего процесса, вы можете использовать следующее:

1
Process.info(self(), :current_stacktrace)

Да, Эликсир тоже может это сделать. Конечно, у вас всегда есть встроенные типы, такие как RuntimeError . Но разве не было бы неплохо, если бы вы могли пойти дальше?

Создать свой собственный тип ошибки легко, используя макрос defexception , который будет удобно принимать опцию :message , чтобы установить сообщение об ошибке по умолчанию, например:

1
2
3
defmodule MyError do
  defexception message: «your custom error has occurred»
end

Вот как это использовать в вашем коде:

1
2
3
4
5
6
iex> try do
…> raise MyError
…> rescue
…> e in MyError -> e
…> end
%MyError{message: «your custom error has occurred»}

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

Мы можем гарантировать, что у конечного пользователя всегда есть подсказка — простое и понятное руководство, которое не сделает его задачу трудной, а наоборот. Сообщения об ошибках должны всегда быть написанным на простом английском языке и дать много информации. Загадочные коды ошибок и имена переменных не годятся для обычных пользователей и могут даже запутать разработчиков!

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

Повысьте точность нашей работы по отладке и включите мониторинг стабильности ваших приложений с помощью этих сторонних сервисов, доступных для Elixir:

  • PpSignal может быть очень полезным для фазы обеспечения качества цикла разработки.
  • GitHub repo Bugsnex — отличный проект для использования интерфейса API с Bugsnag для дальнейшего обнаружения дефектов в вашем приложении Elixir.
  • Honeybadger позволяет отслеживать время безотказной работы, системную память и ошибки, что обеспечивает мониторинг производственных ошибок, поэтому вам не нужно присматривать за своим приложением.

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

Я надеюсь, что вы получили дальнейшее понимание из этого руководства и сможете практически справиться с любыми исключениями, которые вам нужны в вашем приложении Elixir!