Exception Handling in Ruby
In Ruby, error handling works like this; all exceptions and errors are extensions of the Exception
class. While this may seem intuitive, exception handling in Ruby is a touch more nuanced than you might expect thanks to the designed hierarchy of Ruby exceptions.
The begin-rescue
Similar to PHP’s try-catch handler, Ruby’s exception handling begins with the begin-rescue
block. In a nutshell, the begin-rescue
is a code block in Ruby that can be used to deal with raised exceptions without interrupting the Ruby program execution. In other words, you can begin
to execute a block of code, and rescue
any exceptions that are raised.
Rescuing Exceptions
In Ruby by default, begin-rescue
rescues every instance of the StandardError
class. This includes no method errors, type errors, runtime errors, and every custom error that is intended to be rescued within a Ruby application (see Raising Exceptions in Ruby for more information). To rescue every StandardError
, simply wrap the designated section of code in a begin-rescue
block:
begin
# ...
rescue => e
# ...
end
In Ruby when a StandardError
exception is raised within the begin block, an instance of it will be passed to the rescue block as the variable e
(for more information about the structure of Ruby’s Exception
class, see Raising Exceptions in Ruby).
Rescuing Specific Exceptions
While rescuing every exception raised in your Ruby app is great for simplistic implementations—such as generalizing API error responses—best practice is to rescue for specific exceptions. To do this, let’s rewrite the generic Ruby begin-rescue
block above to specifically rescue StandardError
exceptions:
begin
# ...
rescue StandardError => e
# ...
end
Although the difference may be subtle, by following the rescue
command with a class name, then only exceptions of the defined type will be rescued. For example, if we wanted to rescue
all argument errors, we could structure our begin-rescue
block like this:
begin
# ...
rescue ArgumentError => e
# ...
end
But, what if we want to rescue more than one exception type? Much like an if-elsif-else
chain, a begin-rescue
block can have multiple rescues
, which when combined with a check for the StandardError class, allows you to logically adapt to any and all issues that may arise:
begin
# ...
rescue ArgumentError => e
# ...
rescue TypeError => e
# ...
rescue => e
# ...
end
Rescuing All Exceptions
While it may be tempting to rescue every child of the Exception class, it is generally considered bad practice due to the way the Ruby exception hierarchy is structured. The reason for this is that, while all Ruby exceptions and errors are an extension of the Exception
class, many of them are reserved for use internally by Ruby. For example, SignalException::Interrupt
is used to signal that Ctrl-C
has been detected during the execution of a script.
If you were to rescue every child of the Exception class, then the Interrupt exception wouldn’t work as expected. That said, if you do want to rescue every exception that is raised in Ruby, the same begin-rescue
block can be used to specifically rescue all Exception
exceptions:
begin
# ...
rescue Exception => e
# ...
end
Using the above begin-rescue
block in Ruby will rescue every exception and error that is raised, from interrupts to syntax errors, and even memory errors, so use it sparingly and with caution.
How to check Ruby syntax to identify exceptions
rescue
clauses are used to tell Ruby which exception or types of exceptions we want to handle. The syntax for therescue
statement is:
begin
# may raise an exception
rescue AnException
# exception handler
rescue AnotherException
# exception handler
else
# other exceptions
ensure
# always executed
end
The code betweenbegin
andrescue
is where a Ruby exception can occur. If an exception is encountered, the code inside therescue
clause gets executed. For eachrescue
clause, the raised Ruby exception is compared against each parameter and the match succeeds if the exception in the clause is the same as or a superclass of the thrown exception.
If the thrown Ruby exception does not match any of the specified exception types, theelse
block gets executed. Theensure
block is always executed whether a Ruby exception occurs or not.
As an example:
#!/usr/bin/ruby
begin
file = open("/tmp/myfile")
rescue Errno::ENOENT
p "File not found"
else
p "File opened"
end
In the above example, a file is attempted to be opened in thebegin
block. Therescue
block catches a “File not found” Ruby exception in case the file is not found at the location. If the file is found, theelse
block gets executed.
Running the above Ruby code produces the following result if the file is not found:
"File not found"
If the file is found, the following output is produced:
"File not found"
Exception Handling in Ruby on Rails
Generally speaking, the begin-rescue
block works as intended in Ruby on Rails. That said, in order to better handle the specific use cases that can come up in the Rails architecture, additional methods have been made available for use within a Ruby on Rails application.
The rescue_from
The rescue_from directive is an exception handler that rescues exceptions raised in controller actions. The rescue_from directive rescues the specified exceptions raised within a controller, and reacts to those exceptions with a defined method. For example, the following controller rescues User::NotAuthorized
exceptions and passes them to the deny_access()
method:
class ApplicationController < ActionController::Base
rescue_from User::NotAuthorized, with: :deny_access
protected
def deny_access(exception)
# ...
end
end
The advantage to rescue_from is that it abstracts the exception handling away from individual controller actions, and instead makes exception handling a requirement of the controller. This not only makes exception handling within controllers more readable, but also more regimented.
Exception Handling in Sinatra
While the additional exception handling within Ruby on Rails is focused on controller exceptions, Sinatra offers a few additional ways to deal with raised exceptions.
Logging Exceptions with dump_errors
Enabled by default, dump_errors
is a Sinatra setting that allows exception backtraces to be written directly to STDERR
. In the context of a development server, this information can be incredibly valuable, but might be more difficult to act upon in a production environment. When used in conjunction with traditional log aggregation and analysis techniques, however, this is a great way to collect exception data as it happens without needing to reproduce it.
Propagating Exceptions with raise_errors
By default, exceptions raised within Sinatra do not leave the application. What this means is that, when an exception is raised, it is rescued and mapped to internal error handlers. By enabling the raise_errors
setting, these exceptions are raised outside of the application, allowing the server handler or Rack middleware to deal with exceptions.
Enable Classy Error Pages with show_exceptions
When working in a development environment, being able to quickly react to exceptions is crucial, which is why exception backtraces and environment information are on-screen by default in these environments. While this setting is turned off in production environments, it can be enabled or disabled by updating the show_exceptions
setting.
Custom Error Handling
While Sinatra has built-in support for graceful error handling (error handler), it is sometimes desirable to write custom logic to handle raised errors. To do this, an error block can be used in the same way as a rescue block. For example, to rescue a User::NotAuthorized
error, the following directive would work in Sinatra:
error User::NotAuthorized do
# ...
end
Similarly, if we wanted to rescue all exceptions that are raised in a Sinatra application, we could use the following error handler:
error do
# ...
end
Check ruby syntax
If you ever need to check the syntax of your Ruby code here is a great way to do so without executing the Ruby code.
This will check the syntax without executing the program:
ruby -c filename.rb
Исполнение и исключение всегда работают вместе. Если вы открываете файл, которого нет, то вы не справились с этой ситуацией должным образом, ваша программа считается некорректной.
Программа останавливается, если возникает исключение. Таким образом, исключения используются для обработки различных типов ошибок, которые могут возникать во время выполнения программы, и необходимо предпринимать соответствующие действия, а не полностью останавливать программу.
Ruby обеспечивает хороший механизм обработки исключений. Мы прилагаем код, который может вызвать исключение в блоке begin/end и использовать предложения rescue, чтобы сообщить Ruby о типах исключений, которые мы хотим обработать.
Синтаксис
begin # - rescue OneTypeOfException # - rescue AnotherTypeOfException # - else # другие исключения ensure # Всегда будет выполняться end
Все от begin до rescue защищено. Если во время выполнения этого блока кода возникает исключение, управление передается блоку между rescue и end.
Для каждого предложения rescue в begin Ruby сравнивает поднятое исключение с каждым из параметров по очереди. Совпадение завершится успешно, если исключение, указанное в предложении rescue, совпадает с типом создаваемого исключения или является суперклассом этого исключения.
В случае, если исключение не соответствует ни одному из указанных типов ошибок, нам разрешено использовать предложение else после всех предложений rescue.
Пример
#!/usr/bin/ruby begin file = open("/unexistant_file") if file puts "Файл успешно открыт " end rescue file = STDIN end print file, "==", STDIN, "n"
Это приведет к следующему результату. Вы можете видеть, что STDIN заменяется file, потому что произошла ошибка открытия.
#<IO:0xb7d16f84>==#<IO:0xb7d16f84>
Использование утверждения Retry
Вы можете захватить исключение, используя rescue, а затем использовать заявление retry для выполнения блока begin с самого начала.
Синтаксис
begin #Исключения в этом коде #поймал следующий пункт rescue rescue # Этот блок будет захватывать Все типы исключений retry # Это переместит управление в начало <i>begin</i> end
Пример
#!/usr/bin/ruby begin file = open("/unexistant_file") if file puts "Файл успешно открыт" end rescue fname = "existant_file" retry end
Ниже приводится поток процесса:
- Исключение произошло при открытии.
- Вызвано rescue. fname было переназначено.
- retry указал на начало begin.
- Этот файл открывается успешно.
- Продолжал необходимый процесс.
Примечание
Обратите внимание, что если файл повторно замещенного имени не существует, то этот примерный код будет повторятся бесконечно. Будьте осторожны, если вы используете повтор для процесса исключения.
Использование выражения raise
Вы можете использовать заявление raise чтобы сгенерировать исключение. Следующий метод вызывает исключение всякий раз, когда он вызывается. Будет напечатано второе сообщение.
Синтаксис
raise OR raise "Error Message" OR raise ExceptionType, "Error Message" OR raise ExceptionType, "Error Message" condition
Первая форма просто повторно вызывает текущее исключение (или RuntimeError, если нет текущего исключения). Используется в обработчиках исключений, которые должны перехватывать исключение, прежде чем передавать его.
Вторая форма создает новое исключение RuntimeError, устанавливая его сообщение для данной строки. Это исключение затем поднимает стек вызовов.
Третья форма использует первый аргумент для создания исключения, а затем устанавливает связанное сообщение во второй аргумент.
Четвертая форма похожа на третью форму, но вы можете добавить любой условный оператор, например, если не возбуждать исключение.
Пример
#!/usr/bin/ruby begin puts 'Код перед raise.' raise 'Произошла ошибка.' puts 'Код после raise.' rescue puts 'Код в rescued.' end puts 'Код после блока begin.'
Это приведет к следующему результату:
Код после raise. Код в rescued. Код после блока begin.
Еще один пример, показывающий использование raise:
#!/usr/bin/ruby begin raise 'Тестовое исключение.' rescue Exception => e puts e.message puts e.backtrace.inspect end
Это приведет к следующему результату:
Тестовое исключение. ["main.rb:4"]
Использование инструкции ensure
Иногда вам нужно гарантировать, что некоторая обработка выполняется в конце блока кода, независимо от того, было ли возбуждено исключение. Например, у вас может быть файл, открытый при входе в блок, и вам нужно убедиться, что он закрывается по мере выхода из блока.
Предложение ensure делает именно это. обеспечивается после последнего предложения rescue и содержит кусок кода, который всегда будет выполняться по завершении блока. Не имеет значения, нормально ли завершиться блок, если есть исключения raises и rescues, или если он завершается исключением, блок ensure будет запущен.
Синтаксис
begin #.. процесс #..поднять исключение rescue #.. ошибка обработки ensure #.. наконец, убедитесь в выполнении #.. Это всегда будет выполняться. end
Пример
begin raise 'Тестовое исключение.' rescue Exception => e puts e.message puts e.backtrace.inspect ensure puts "Обеспечение выполнения" end
Это приведет к следующему результату:
Тестовое исключение. ["main.rb:4"] Обеспечение выполнения
Использование инструкции else
Если предложение else присутствует, оно выполняется после предложений rescue и до того, как оно будет выполнено.
Тело предложения else выполняется только в том случае, если в основной части кода не возникают исключения.
Синтаксис
begin #.. процесс #.. вызывается исключение rescue # .. ошибка обработки else #.. выполняется, если нет исключения ensure #.. наконец, убедитесь в выполнении #.. Это всегда будет выполняться. end
Пример
begin # raise 'Тестовое исключение.' puts "Я не поднимаю исключение" rescue Exception => e puts e.message puts e.backtrace.inspect else puts "Поздравляем - ошибок нет!" ensure puts "Обеспечение выполнения" end
Это приведет к следующему результату:
Я не поднимаю исключение Поздравляем - ошибок нет! Обеспечение выполнения
Сообщение “Поднятая ошибка” можно записать с помощью переменной $!.
Catch и Throw
В то время как механизм исключения и rescue отлично подходит для отказа от выполнения, когда что-то идет не так, иногда бывает приятно выпрыгнуть из какой-то глубоко вложенной конструкции во время нормальной обработки. Это – то, где может пригодится catch и throw.
catch определяет блок, который помечен с данным именем (которое может быть символ или строка). Блок выполняется нормально до тех пор, пока не будет обнаружен throw.
Синтаксис
throw :lablename #.. это не будет выполнено catch :lablename do #.. соответствующий catch будет выполняться после обнаружения throw. end OR throw :lablename condition #.. это не будет выполнено catch :lablename do #.. соответствующий catch будет выполняться после обнаружения throw. end
Пример
В следующем примере используется throw для прекращения взаимодействия с пользователем, если ‘!’ набирается в ответ на любое приглашение.
def promptAndGet(prompt) print prompt res = readline.chomp throw :quitRequested if res == "!" return res end catch :quitRequested do name = promptAndGet("Имя: ") age = promptAndGet("Возраст: ") sex = promptAndGet("Пол: ") # .. # process information end promptAndGet("Имя:")
Вы должны попробовать вышеуказанную программу на своем компьютере, потому что она требует ручного взаимодействия. Это приведет к следующему результату:
Имя: Ruby on Rails Возраст: 18 Пол: ! Имя: AndreyEx
Класс Exception
Стандартные классы и модули Ruby вызывают исключения. Все классы исключений образуют иерархию с классом Exception в верхней части. Следующий уровень содержит семь разных типов:
- Interrupt
- NoMemoryError
- SignalException
- ScriptError
- StandardError
- SystemExit
На этом уровне есть еще одно исключение, Fatal, но интерпретатор Ruby использует это только внутренне.
И ScriptError, и StandardError имеют ряд подклассов, но здесь нам не нужно вдаваться в подробности. Важно то, что если мы создаем собственные классы исключений, они должны быть подклассами любого класса Exception или одного из его потомков.
Давайте посмотрим на пример:
class FileSaveError < StandardError attr_reader :reason def initialize(reason) @reason = reason end end
Теперь рассмотрим следующий пример, который будет использовать это исключение:
File.open(path, "w") do |file| begin # Запишите данные ... rescue # Что-то пошло не так! raise FileSaveError.new($!) end end
Важной линией здесь является поднять FileSaveError.new ($!). Мы вызываем raise, чтобы сигнализировать о том, что произошло исключение, передав ему новый экземпляр FileSaveError, по причине того, что конкретное исключение вызвало сбой записи данных.
Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.
Exceptions in Ruby
In Ruby, Exceptions are created using raise command:
1begin 2 raise "boom" 3end
If we execute that code then we will see the error:
Notice that Ruby is saying that it is a RuntimeError. Here is Ruby’s official
documentation about
RuntimeError. Above code
can also be written like this:1raise RuntimeError, "boom"
As per Ruby’s documentation if when we do not mention any class while raising an
exception then by default it is RuntimeError class.In Ruby all exceptions are subclasses of Exception class.
Hierarchy of Ruby Exception class
Ruby has lots of built in exceptions. Here is hierarchy of all Ruby’s
exceptions:1Exception 2 NoMemoryError 3 ScriptError 4 LoadError 5 NotImplementedError 6 SyntaxError 7 SecurityError 8 SignalException 9 Interrupt 10 StandardError 11 ArgumentError 12 UncaughtThrowError 13 EncodingError 14 FiberError 15 IOError 16 EOFError 17 IndexError 18 KeyError 19 StopIteration 20 LocalJumpError 21 NameError 22 NoMethodError 23 RangeError 24 FloatDomainError 25 RegexpError 26 RuntimeError 27 SystemCallError 28 Errno::* 29 ThreadError 30 TypeError 31 ZeroDivisionError 32 SystemExit 33 SystemStackError
The rescue method catches a class and all its subclasses
1begin 2 do_something 3rescue NameError 4end
Here we are rescuing all NameError exceptions. However NoMethodError will
also be rescued because NoMethodError is a subclass of NameError.For example, consider a api_exceptions.rb file where we are rescuing from all
the exceptions like so:1module ApiExceptions 2 rescue_from StandardError, with: :handle_api_exception 3 rescue_from ActiveRecord::RecordNotUnique, with: :handle_record_not_unique_exception 4 5 def handle_api_exception(exception) 6 case exception 7 # handles the exceptions 8 end 9 10 def handle_record_not_unique_exception(exception) 11 # handles record not unique exception 12 end 13end
The RecordNotUnique exception is a child class of StandardError. When we
rescue from StandardError all the child exception classes will also be caught.
So the rescue_from ActiveRecord::RecordNotUnique in the above example is
redundant. The code will never reach this line because the RecordNotUnique
exception will already be caught by the rescue_from StandardError statement.The correct way of rescuing the errors will be, like so:
1module ApiExceptions 2 rescue_from StandardError, with: :handle_api_exception 3 4 def handle_api_exception(exception) 5 case exception 6 7 when ActiveRecord::RecordNotFound 8 handle_record_not_unique_exception(exception) 9 10 # handles the other exceptions 11 end 12 13 def handle_record_not_unique_exception(exception) 14 # handles record not unique exception 15 end 16end
In the above example, we are rescuing StandardError and all its children
classes with handle_api_exception method. And inside handle_api_exception
method case is used to handle
the required child exception classes specifically.Certain exceptions and when they are raised
-
Pundit::NotAuthorizedError is raised when any authorization error from
Pundit gem raises and we
can handle it with 403 or forbidden status. -
ActionController::ParameterMissing is raised when a required parameter is
missing. Consider a case when we have strong params defined for a post
request and those params are missing in the request header. We can handle this
via 500 or internal_server_error status. -
ActiveRecord::RecordNotFound is raised when Rails does not find any
record. When we use find method to search for a record with the provided
id in params and the id is mistaken or the record is missing from DB,
Rails raises ActiveRecord::RecordNotFound exception. We can handle this
with a 404 or not_found status. -
ActiveRecord::RecordInvalid is raised on failure of validations declared in
model for any record creation or updation. Let’s say we have some email
validation declared in model and the input email does not match with the given
Regex pattern Rails raises ActiveRecord::RecordInvalid exception. We can
handle this with a 422 or unprocessable_entity status. -
ActiveRecord::RecordNotUnique is raised when a record cannot be inserted or
updated because it would violate a uniqueness constraint from DB. We can
handle this with unprocessable_entity or 422 status. -
PG::NotNullViolation: ERROR: null value in column «name» of relation «users» violates not-null constraint
for missing mandatory input. We should handle the database errors starting
with PG:: or SQLite3:: with internal_server_error or 500 status. If we
keep this as unprocessable_entity then it won’t raise Honeybadger issues,
meaning it will log the error silently. These errors are very rare and we
should be getting notified of these errors by Honeybadger in Github.
Raising error using class
Following two lines do the same thing:
1raise "boom" 2raise RuntimeError, "boom"
We can raise exceptions of a particular class by stating the name of that
exception class:1raise ArgumentError, "two arguments are needed" 2raise LoadError, "file not found"
Default rescue is StandardError
rescue without any argument is same as rescuing StandardError:
Above statement is same as the one given below:
1begin 2rescue StandardError 3end
Catching multiple types of exceptions in one shot
We can catch multiple types of exceptions in one statement:
1begin 2rescue ArgumentError,NameError 3end
Catching exception in a variable
We can catch exception in a variable like this:
1begin 2rescue StandardError => e 3end
Here e is an exception object. The three main things we like to get from an
exception object are «class name», «message» and «backtrace».Let’s print all the three values:
1begin 2 raise "boom" 3rescue StandardError => e 4 puts "Exception class is #{e.class.name}" 5 puts "Exception message is #{e.message}" 6 puts "Exception backtrace is #{ e.backtrace}" 7end
Custom exceptions
Sometimes we need custom exceptions. Creating custom exceptions is easy:
1class NotAuthorizedError < StandardError 2end 3 4raise NotAuthorizedError.new("You are not authorized to edit record")
NotAuthorizedError is a regular Ruby class. We can add more attributes to it
if we want:1class NotAuthorizedError < StandardError 2 attr_reader :account_id 3 4 def initialize(message, account_id) 5 #invoke the constructor of parent to set the message 6 super(message) 7 8 @account_id = account_id 9 end 10end 11 12raise NotAuthorizedError.new("Not authorized", 171)
rescue nil
Sometimes we see code like this:
The above code is equivalent to the following code:
1begin 2 do_something 3rescue 4 nil 5end
The above code can also be written like so, since by default StandardError is
raised:1begin 2 do_something 3rescue StandardError 4 nil 5end
Exception handling in Ruby on Rails using rescue_from
A typical controller could look like this:
1class ArticlesController < ApplicationController 2 def show 3 @article = Article.find(params[:id]) 4 rescue ActiveRecord::RecordNotFound 5 render_404 6 end 7 8 def edit 9 @article = Article.find(params[:id]) 10 rescue ActiveRecord::RecordNotFound 11 render_404 12 end 13end
We can use
rescue_from
to catch the exception.The rescue_from directive is an exception handler that rescues the specified
exceptions raised within controller actions and reacts to those exceptions with
a defined method.For example, the following controller rescues ActiveRecord::RecordNotFound
exceptions and passes them to the render_404 method:1class ApplicationController < ActionController::Base 2 rescue_from ActiveRecord::RecordNotFound, with: :render_404 3 4 def render_404 5 end 6end 7 8class ArticlesController < ApplicationController 9 def show 10 @article = Article.find(params[:id]) 11 end 12 13 def edit 14 @article = Article.find(params[:id]) 15 end 16end
The advantage to rescue_from is that it abstracts the exception handling away
from individual controller actions, and instead makes exception handling a
requirement of the controller.The rescue_from directive not only makes exception handling within controllers
more readable, but also more regimented.Rescuing from specific exception
Ruby’s Exception is the parent class to all errors. So one might be tempted to
always rescue from this exception class and get the «job» done. But DON’T!Exception includes the class of errors that can occur outside your
application. Things like memory errors, or SignalException::Interrupt(sent
when you manually quit your application by hitting Control-C), etc. These are
the errors that you don’t want to catch in your application as they are
generally serious and related to external factors. Rescuing the Exception
class can cause very unexpected behaviour.StandardError is the parent of most Ruby and Rails errors. If you catch
StandardError you’re not introducing the problems of rescuing Exception, but
it is not a great idea either. Rescuing all application-level errors might cover
up unrelated bugs that you don’t know about.The safest approach is to rescue the error(or errors) you are expecting and deal
with the consequences of that error inside the rescue block.In the event of an unexpected error in your application you want to know that a
new error has occurred and deal with the consequences of that new error inside
its own rescue block.Being specific with rescue means your code doesn’t accidentally swallow new
errors. You avoid subtle hidden errors that lead to unexpected behaviour for
your users and bug hunting for you.Do not use exception as control flow
Let’s look at the following code:
1class QuizController < ApplicationController 2 def load_quiz 3 @quiz = current_user.quizzes.find(params[:id]) 4 rescue ActiveRecord::RecordNotFound 5 format.json { render status: :not_found, json: { error: "Quiz not found"}} 6 end 7end
In the above code when quiz id is not found then an exception is raised and then
that exception is immediately caught.Here the code is using exception as a control flow mechanism. What it means is
that the code is aware that such an exception could be raised and is prepared to
deal with it.The another way to deal with such a situation would be to not raise the
exception in the first place. Here is an alternative version where code will not
be raising any exception:1class QuizController < ApplicationController 2 def load_quiz 3 @quiz = current_user.quizzes.find_by_id(params[:id]) 4 unless @quiz 5 format.json { render status: :not_found, json: { error: "Quiz not found"}} 6 end 7 end 8end
In the above case instead of using find code is using find_by_id which would
not raise an exception in case the quiz id is not found.In Ruby world we like to say that an exception should be an exceptional
case. Exceptional case could be database is down or there is some network
error. Exception can happen anytime but in this case code is not using catching
an exception as a control flow.Long time ago in the software engineering world GOTO was used a lot. Later
Edsger W. Dijkstra wrote a
famous letter
Go To Statement Considered Harmful.
Today it is a well established that using GOTO is indeed harmful.Many consider using Exception as a control flow similar to using GOTO since when
an exception is raised it breaks all design pattern and exception starts flowing
through the stack. The first one to capture the exception gets the control of
the software. This is very close to how GOTO works. In Ruby world it is well
established practice to
not to use Exception as a control flow.Using bang methods in controller actions
But, just like everything in software engineering, the suggestion we had made in
last section also has some exceptions. Like in controllers etc, we should be
trying our level best to keep controllers as skinny as possible. So adding
repetitive unless or if statements as a replacement for rescue statements
won’t scale. Thus in such cases, what we should do is use the bang(!) versions
of ActiveRecord methods, like create!, update!, destroy! or save! within
the controller actions. This would raise an exception in case there’s a failure.But where to handle theses exceptions? Well, in the chapter where we had cleaned
up the application controller, we had added a concern named ApiExceptions.
This concern is included in the ApplicationController. Which means all other
controllers will be having access to the methods defined in that concern. In
that concern we have several rescue_from statements, which handles specific
exceptions.Thus in our controller, we could write something like so, and both the success
and failure cases will be handled:1class QuizController < ApplicationController 2 before_action :load_quiz!, only: %i[update] 3 4 def update 5 @quiz.update!(quiz_params) # control wouldn't even reach here if exception was raised in load_quiz! 6 respond_with_success(t("successfully_created", entity: "Quiz")) # control will only reach here if the above statement didn't raise any exception 7 end 8 9 private 10 11 def load_quiz! 12 @quiz = current_user.quizzes.find_by!(id: params[:id]) 13 end 14end
Notice how we have named the method as load_quiz! with a bang(!) rather than
simply load_quiz? It’s to denote that the particular method has the potential
to raise an exception. If no record is found by the find_by! method then a
ActiveRecord::RecordNotFound exception will be raised.There is a problem in the above code block, which has to deal with code
conventions that we follow in BigBinary. That’s, if we are querying using the
id attribute only then we should use the find method over find_by! because
in the find method we can directly pass the id value without defining any
key and it makes the code cleaner. If for a given id no record is found by
find method then the same exception will be raised that is
ActiveRecord::RecordNotFound.So, in the above code block the load_quiz! method needs to be updated like so:
1def load_quiz! 2 @quiz = current_user.quizzes.find(id) 3end
There is nothing to commit in this chapter since all we had done was
learning the basics of exception handling in Ruby.
Are you sick and tired of handling endless exceptions, writing custom logic to handle bad API requests and serializing the same errors over and over?
What if I told you there was a way to abstract away messy and repetitive error raising and response rendering in your Rails API? A way for you to write just one line of code (okay, two lines of code) to catch and serialize any error your API needs to handle? All for the low low price of just $19.99!
Okay, I’m kidding about that last part. This is not an infomercial.
Although Liz Lemon makes the snuggie (sorry, «slanket») look so good, just saying.
In this post, we’ll come to recognize the repetitive nature of API error response rendering and implement an abstract pattern to DRY up our code. How? We’ll define a set of custom errors, all subclassed under the same parent and tell the code that handles fetching data for our various endpoints to raise these errors. Then, with just a few simple lines in a parent controller, we’ll rescue any instance of this family of errors, rendering a serialized version of the raised exception, thus taking any error handling logic out of our individual endpoints.
I’m so excited. Let’s get started!
Recognizing the Repetition in API Error Response Rendering
The API
For this post, we’ll imagine that we’re working on a Rails API that serves data to a client e-commerce application. Authenticated users can make requests to view their past purchases and to make a purchase, among other things.
We’ll say that we have the following endpoints:
POST '/purchases'
GET '/purchases'
Any robust API will of course come with specs.
API Specs
Our specs look something like this:
Purchases
Request
GET api/v1/purchases
# params
{
start_date: " ",
end_date: " "
}
Success Response
# body
{
status: "success",
data: {
items: [
{
id: 1,
name: "rice cooker",
description: "really great for cooking rice",
price: 14.95,
sale_date: "2016-12-31"
},
...
]
}
}
# headers
{"Authorization" => "Bearer <token>"}
Error Response
{
status: "error",
message: " ",
code: " "
}
code | message |
3000 | Can’t find purchases without start and end date |
Yes, I’ve decided querying purchases requires a date range. I’m feeling picky.
Request
POST api/v1/purchases
# params
{
item_id: 2
}
Success Response
# body
{
status: "success",
data: {
purchase_id: 42,
item_id: 2
purchase_status: "complete"
}
}
# headers
{"Authorization" => "Bearer <token>"}
Error Response
{
status: "error",
message: " ",
code: " "
}
code | message |
4000 | item_id is required to make a purchase |
Error Code Pattern
With just a few endpoint specs, we can see that there is a lot of shared behavior. For the GET /purchases
request and POST /purchases
requests, we have two specific error scenarios. BUT, in both of the cases in which we need to respond with an error, the response format is exactly the same. It is only the content of the code
and message
keys of our response body that needs to change.
Let’s take a look at what this error handling could look like in our API controllers.
API Controllers
# app/controllers/api/v1/purchases_controller.rb module Api module V1 class PurchasesController < ApplicationController def index if params[:start_date] && params[:end_date] render json: current_user.purchases else render json: {status: "error", code: 3000, message: "Can't find purchases without start and end date"} end end def create if params[:item_id] purchase = Purchase.create(item_id: params[:item_id], user_id: current_user.id) render json: purchase else render json: {status: "error", code: 4000, message: "item_id is required to make a purchase} end end end end end
Both of our example endpoints contain error rendering logic and they are responsible for composing the error to be rendered.
This is repetitious, and will only become more so as we build additional API endpoints. Further, we’re failing to manage our error generation in a centralized away. Instead creating individual error JSON packages whenever we need them.
Let’s clean this up. We’ll start by building a set of custom error classes, all of which will inherit from the same parent.
Custom Error Classes
All of our custom error classes will be subclassed under ApiExceptions::BaseException
. This base class will contain our centralized error code map. We’ll put our custom error classes in the lib/
folder.
# lib/api_exceptions/base_exception.rb module ApiExceptions class BaseException < StandardError include ActiveModel::Serialization attr_reader :status, :code, :message ERROR_DESCRIPTION = Proc.new {|code, message| {status: "error | failure", code: code, message: message}} ERROR_CODE_MAP = { "PurchaseError::MissingDatesError" => ERROR_DESCRIPTION.call(3000, "Can't find purchases without start and end date"), "PurchaseError::ItemNotFound" => ERROR_DESCRIPTION.call(4000, "item_id is required to make a purchase") } def initialize error_type = self.class.name.scan(/ApiExceptions::(.*)/).flatten.first ApiExceptions::BaseException::ERROR_CODE_MAP .fetch(error_type, {}).each do |attr, value| instance_variable_set("@#{attr}".to_sym, value) end end end end
We’ve done a few things here.
- Inherit
BaseException
fromStandardError
, so that instances of our class can be raised and rescued. - Define an error map that will call on a proc to generate the correct error code and message.
- Created
attr_reader
s for the attributes we want to serialize - Included
ActiveModel::Serialization
so that instances of our class can be serialized by Active Model Serializer. - Defined an
#initialize
method that will be called by all of our custom error child classes. When this method runs, each child class will use the error map to set the correct values for the@status
,@code
and@message
variables.
Now we’ll go ahead and define our custom error classes, as mapped out in our error map.
# lib/api_exceptions/purchase_error.rb
module ApiExceptions
class PurchaseError < ApiExceptions::BaseException
end
end
# lib/api_exceptions/purchase_error/missing_dates_error.rb
module ApiExceptions
class PurchaseError < ApiExceptions::BaseException
class MissingDatesError < ApiExceptions::PurchaseError
end
end
end
# lib/api_exceptions/purchase_error/item_not_found.rb
module ApiExceptions
class PurchaseError < ApiExceptions::BaseException
class ItemNotFound < ApiExceptions::PurchaseError
end
end
end
Now that are custom error classes are defined, we’re ready to refactor our controller.
Refactoring The Controller
For this refactor, we’ll just focus on applying our new pattern to a single endpoint, since the same pattern can be applied again and again. We’ll take a look at the POST /purchases
request, handled by PurchasesController#create
Instead of handling our login directly in the controller action, we’ll build a service to validate the presence of item_id
. The service should raise our new custom ApiExceptions::PurchaseError::ItemNotFound
if there is no item_id
in the params.
module Api module V1 class PurchasesController < ApplicationController ... def create purchase_generator = PurchaseGenerator.new(user_id: current_user.id, item_id: params[:item_id]) render json: purchase_generator end end end end
Our service is kind of like a service-model hybrid. It exists to do a job for us––generate a purchase––but it also needs a validation and it will be serialized as the response body to our API request. For this reason, we’ll define it in app/models
# app/models class PurchaseGenerator include ActiveModel::Serialization validates_with PurchaseGeneratorValidator attr_reader :purchase, :user_id, :item_id def initialize(user_id:, item_id:) @user_id = user_id @item_id = item_id @purchase = Purchase.create(user_id: user_id, item_id: item_id) if valid? end end
Now, let’s build our custom validator to check for the presence of item_id
and raise our error if it is not there.
class PostHandlerValidator < ActiveModel::Validator def validate(record) validate_item_id end def validate_item_id raise ApiExceptions::PurchaseError::ItemNotFound.new unless record.item_id end end
This custom validator will be called with the #valid?
method runs.
So, the very simple code in our Purchases Controller will raise the appropriate error if necessary, without us having to write any control flow in the controller itself.
But, you may be wondering, how will we rescue or handle this error and render the serialized error?
Universal Error Rescuing and Response Rendering
This part is really cool. With the following line in our Application Controller, we can rescue *any error subclassed under ApiExceptions::BaseException
:
class ApplicationController < ActionController::Base rescue_from ApiExceptions::BaseException, :with => :render_error_response end
This line will rescue any such errors by calling on a method render_error_response
, which we’ll define here in moment, and passing that method the error that was raised.
all our render_error_response
method has to do and render that error as JSON.
class ApplicationController < ActionController::Base rescue_from ApiExceptions::BaseException, :with => :render_error_response ... def render_error_response(error) render json: error, serializer: ApiExceptionsSerializer, status: 200 end end
Our ApiExceptionSerializer
is super simple:
class ApiExceptionSerializer < ActiveModel::Serializer
attributes :status, :code, :message
end
And that’s it! We’ve gained super-clean controller actions that don’t implement any control flow and a centralized error creation and serialization system.
Let’s recap before you go.
Conclusion
We recognized that, in an API, we want to follow a strong set of conventions when it comes to rendering error responses. This can lead to repetitive controller code and an endlessly growing and scattered list of error message definitions.
To eliminate these very upsetting issues, we did the following:
- Built a family of custom error classes, all of which inherit from the same parent and are namespaced under
ApiExceptions
. - Moved our error-checking control flow logic out of the controller actions, and into a custom model.
- Validated that model with a custom validator that raises the appropriate custom error instance when necessary.
- Taught our Application Controller to rescue any exceptions that inherit from
ApiExceptions::BaseException
by rendering as JSON the raised error, with the help of our customApiExceptionSerializer
.
Keep in mind that the particular approach of designing a custom model with a custom validator to raise our custom error is flexible. The universally applicable part of this pattern is that we can build services to raise necessary errors and call on these services in our controller actions, thus keeping error handling and raising log out of individual controller actions entirely.
Модульный подход к обработке ошибок в Rails.
Закон Мерфи:
Как гласит закон Мерфи, все, что может пойти не так, пойдет не так, поэтому важно быть к этому готовым. Это применимо везде, даже в разработке программного обеспечения. Приложение, которое мы разрабатываем, должно быть достаточно надежным, чтобы справиться с этим. Другими словами, он должен быть устойчивым. Это именно то, о чем этот пост в блоге.
Все, что может пойти не так, пойдет не так.
— Закон Мерфи
В типичном рабочем процессе Rails мы обрабатываем ошибки на уровне контроллера. Допустим, вы пишете API с помощью Rails. Рассмотрим следующий метод контроллера для визуализации пользовательского JSON.
Когда пользовательский объект найден, он отображает его как json, в противном случае отображает ошибку json. Это типичный способ написания метода show — это Rails. Но вот в чем загвоздка. Если запись пользователя не найдена, она не попадает в блок else, а отображает резервный контент 500.html. Что ж, это было неожиданно. Это связано с тем, что если запись не найдена, возникает ошибка RecordNotFound. То же самое и с find_by! или любые методы поиска на ура.
Исключение! = Ошибка
Прежде чем мы приступим к исправлению ошибок, нам нужно понять кое-что важное. Как видно из приведенного выше примера, мы получаем ошибку ActiveRecord :: RecordNotFound. Блок try catch в ruby будет выглядеть примерно так, что отлично работает.
Но когда вы хотите спастись от всех исключений, действительно важно знать разницу между исключениями и ошибками в Ruby. Никогда не выполнять восстановление из исключений. Он пытается обработать каждое исключение, наследуемое от класса Exception, и в конечном итоге останавливает выполнение.
Вместо этого нам нужно спастись от StandardError. Вот отличное сообщение в блоге, объясняющее разницу http://blog.honeybadger.io/ruby-exception-vs-standarderror-whats-the-difference/.
Спасение
Для обработки ошибок мы можем использовать блок спасения. Блок восстановления аналогичен блоку try..catch, если вы из мира Java. Вот тот же пример со спасательным блоком.
При таком подходе ошибки устраняются в методах контроллера. Хотя это работает отлично, это может быть не лучший подход для обработки ошибок. Вот несколько причин рассмотреть альтернативный подход.
- Толстые контроллеры: обязательно прочтите эту отличную статью Thoughtbots https://robots.oughttbot.com/skinny-controllers-skinny-models.
- Принцип DRY: мы просто повторяем блокировку ошибки в разных местах, что противоречит принципу DRY (не повторяйся).
- Ремонтопригодность: сложнее поддерживать код. Изменения ошибки, такие как формат, повлекут за собой серьезные изменения.
Альтернативный подход — переместить блок обработки ошибок в ApplicationController. Более чистый подход — написать модуль обработчика ошибок.
Обработка ошибок — модульный подход
Чтобы обрабатывать ошибки в одном месте, наш первый вариант — написать в ApplicationController. Но лучше всего отделить его от логики приложения.
Давайте создадим модуль, который обрабатывает ошибки на глобальном уровне. Создайте модуль ErrorHandler (error_handler.rb) и поместите его в папку lib / error (или в другое место для загрузки), а затем включите его в наш ApplicationController.
Важно: загрузите модуль ошибок при запуске приложения, указав его в config / application.rb.
Примечание. Я использую несколько вспомогательных классов для рендеринга вывода json. Вы можете проверить это здесь.
Прежде чем приступить к работе с модулем error_handler, вот действительно интересная статья о модулях, которую вам обязательно стоит прочитать. Если вы заметили, что метод self.included в модуле работает так же, как если бы он был помещен в исходный класс. Поэтому все, что нам нужно сделать, это включить модуль ErrorHandler в ApplicationController.
Давайте проведем рефакторинг ErrorModule для размещения нескольких блоков обработки ошибок. Так он выглядит намного чище.
Если вы заметили ошибку ActiveRecord: RecordNotFound, она также наследует StandardError. Поскольку у нас есть механизм спасения, мы получаем: record_not_found. Блок StandardError действует как резервный механизм, который обрабатывает все ошибки.
Определите собственное исключение.
Мы также можем определить наши собственные классы ошибок, которые наследуются от StandardError. Для простоты мы можем создать класс CustomError, который содержит общие переменные и методы для всех определенных пользователем классов ошибок. Теперь наша UserDefinedError расширяет CustomError.
Мы можем переопределить методы, специфичные для каждой ошибки. Например, NotVisibleError расширяет CustomError. Как вы могли заметить, мы переопределяем error_message.
Для обработки всех ошибок, определенных пользователем, все, что нам нужно сделать, это спастись от CustomError. Мы также можем спастись от конкретной ошибки, если хотим обработать ее по-другому.
404 и 500
Вы можете обрабатывать распространенные исключения, такие как 404 и 500, хотя это полностью зависит от разработчика. Нам нужно создать для него отдельный класс контроллера, ErrorsController.
Скажите Rails использовать маршруты для разрешения исключений. Нам просто нужно добавить следующую строку в application.rb.
config.exceptions_app = routes
Теперь исключения 404 возвращаются к errors # not_found, а 500 — к errors # internal_server_error.
Заключительные примечания
Модульный подход — это способ обработки ошибок в Rails. Всякий раз, когда мы хотим изменить конкретное сообщение / формат об ошибке, нам просто нужно изменить его в одном месте. При таком подходе мы также отделяем логику приложения от обработки ошибок, тем самым делая Контроллеры Slick вместо Fat.
В Rails лучше всего использовать Skinny Controllers & Models .
Вот полный Исходный код для модульного подхода к обработке ошибок. Пожалуйста, нажмите кнопку «Рекомендовать», если вы сочли это полезным. И, как всегда, не стесняйтесь отвечать, если у вас есть сомнения. Ваше здоровье!
Способ 1: http://www.uedidea.com/rails%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86.html
def add_to_cart
product = Product.find(params[:id])
спасение ActiveRecord :: RecordNotFound #intercept Product.find () исключение
logger.error («Попытка получить доступ к недопустимому продукту # {params [: id]}») # Журнал
flash [: note] = "Недопустимый продукт" # примечание Информация о напоминании магазина
redirect_to: action => "index" # Перейти к индексу
end
begin
@user.destroy
flash[:notice] = "User #{@user.name} deleted"
Rescue Exception => e #catch, если исключение происходит в приведенном выше выполнении кода
flash[:notice] = e.message
end
begin
raise ArgumentError, "Bad data"
rescue => err
puts err
ensure
... # Выполнить очистку
end
Метод 2: around_filter
http://hlee.iteye.com/blog/323025
around_filter :rescue_record_not_found
def rescue_record_not_found
begin
yield
rescue ActiveRecord::RecordNotFound
render :file => "#{RAILS_ROOT}/public/404.html"
end
end
Хле также упомянул в статье, что это можно сделать:
rescue_from ActiveRecord::RecordNotFound, with => :rescue_record_not_found
def rescue_record_not_found
render :file => "#{RAILS_ROOT}/public/404.html"
end
Способ 3: rescue_action_in_public
Ссылка: http://202.199.224.30/post/article/7
def rescue_action_in_public(exception)
logger.error("rescue_action_in_public executed")
case exception
when ActiveRecord::RecordNotFound, ::ActionController::RoutingError,
::ActionController::UnknownAction
logger.error("404 displayed")
render(:file => "#{RAILS_ROOT}/public/404.html",
:status => "404 Not Found")
else
logger.error("500 displayed")
render(:file => "#{RAILS_ROOT}/public/500.html",
:status => "500 Error")
# SystemNotifier.deliver_exception_notification(self, request,
# exception)
end
end
Примечание. Конфигурация в разных средах, в производственной среде, конфигурация по умолчанию должна отображать эффект, но в режиме разработки вам необходимо подтвердить
code
config.action_controller.consider_all_requests_local = false
Если необходимо добавить локальный доступ (app / controllers / application.rb):
code
def local_request?
false
end
Ссылка на тип ошибки:
DEFAULT_RESCUE_RESPONSE = :internal_server_error
DEFAULT_RESCUE_RESPONSES = {
'ActionController::RoutingError' => :not_found,
'ActionController::UnknownAction' => :not_found,
'ActiveRecord::RecordNotFound' => :not_found,
'ActiveRecord::StaleObjectError' => :conflict,
'ActiveRecord::RecordInvalid' => :unprocessable_entity,
'ActiveRecord::RecordNotSaved' => :unprocessable_entity,
'ActionController::MethodNotAllowed' => :method_not_allowed,
'ActionController::NotImplemented' => :not_implemented,
'ActionController::InvalidAuthenticityToken' => :unprocessable_entity
}
DEFAULT_RESCUE_TEMPLATE = 'diagnostics'
DEFAULT_RESCUE_TEMPLATES = {
'ActionView::MissingTemplate' => 'missing_template',
'ActionController::RoutingError' => 'routing_error',
'ActionController::UnknownAction' => 'unknown_action',
'ActionView::TemplateError' => 'template_error'
}
Прочитайте обсуждение в этом посте еще раз: http://www.iteye.com/topic/708334
Обработка ошибок в рельсах требует дальнейшего изучения.
There are various ways to rescue from exceptions raised in Rails App.
The most basic is
begin ......... rescue NameOfException => exc logger.error("Message for the log file #{exc.message}") flash[:notice] = "Store error message" redirect_to(:action => 'index') end
Or you can render a static HTML file namely public/401.html
or public/400.html.erb
rescue NameOfException => exc logger.error("Message for the log file #{exc.message}") flash[:notice] = "Store error message" render file: "#{Rails.root.to_s}/public/401.html", status: :unauthorized end
Global Exception Handling in Rails
To handle globally (in application_controller.rb) or scoped to specific controller you can use the following syntax:
rescue_from CanCan::AccessDenied do render file: "#{Rails.root.to_s}/public/401.html", status: :unauthorized end
rescue_from ActiveRecord::RecordNotFound do |exception| # It show the error in development mode throw exception if Rails.env === 'development' render file: "#{Rails.root.to_s}/public/400.html", status: :bad_request end
or
if params[:url_code] && @pact.nil? raise Pundit::NotAuthorizedError, "You are not authorized to perform this action." else
you can pass the error message like in the above example
syntax is
raise ExceptionClass, "Context specific error message"
and you can access this message in the exception handling method param.
rescue_from ExceptionClass, with: :oops_something_happened
def oops_something_happened(error = OpenStruct.new({message: nil})) flash[:error] = error.message || "You are not authorized to perform this action." redirect_to(request.referrer || root_path) end
More Better Way:
To change this behavior of Rails to handle specific type of exception we need to modify the application’s config file. /config/application.rb
config.action_dispatch.rescue_responses["ProductsController::Forbidden"] = :forbidden
config.action_dispatch.rescue_responses["ActiveRecord::RecordNotFound"] = :forbidden
Here we set config.action_dispatch.rescue_responses
which is a hash where the key represents the name of the exception that we want to handle and the value is the status code, in this case :forbidden
(we can use the status codes that Rack uses instead of a numeric HTTP status). To learn more about how Rack handles status codes take a look at the documentation for Rack::Utils
where we’ll find the names for each status code and how it’s converted to a symbol. If we visit the a product’s page now after restarting our app we’ll see a 403.html
error page.
Next we’ll show you some of the exceptions that Rails maps by default. For example if we trigger a route that doesn’t exist we’ll see a 404
error instead of a 500
. Most of these are defined in the Rails source code in the ExceptionWrapper
class. This class sets the default rescue_responses
hash that we configured earlier and one of the values set is ActionController::RoutingError
which is set to :not_found
. This is what we see in the application with the 404
status.
Note:
For this feature you must have files like 400.html, 404.html, etc inside the public dir.
Catch and Throw in Ruby
Jan 24, 2012
In my last post I had a look at handling exceptions in Ruby and the functionality around that. In this post I’m going to look at how Ruby also allows us to unwind a single block of code without raising an exception. I’ve said before that exceptions should be exceptional – the functionality in Ruby offers a lightweight version of this within a single scope.
Let’s see some code
This is much easier to explain with an example.
def get_number
rand(100)
end
random_numbers = catch (:random_numbers) do
result = []
10.times do
num = get_number
throw :random_numbers if num < 10
result << num
end
result
end
p random_numbers
The catch keyword defines a block with a given name. The block is processed normally until a throw statement is encountered.
When a throw statement is encountered Ruby will look up the call stack for a catch statement with the corresponding symbol. It will then unwind the call stack up to that point and terminate the block.
In this example the block will have two possible return values. If no number under 10 is generated the throw statement will never execute and the array of random numbers will be returned. If the throw statement does execute the block will simply return nil. Ruby also allows us to override this behavior – for example, instead of returning nil I might choose to return an empty array. We can do this by specifying a second parameter to the throw statement.
random_numbers = catch (:random_numbers) do
result = []
10.times do
num = get_number
throw(:random_numbers, []) if num < 10
result << num
end
result
end
Performance
One of the reasons why it’s possibly not a good idea to overuse exceptions in your code is performance. Every time you raise an exception Ruby has to build a stack trace. If your exceptions are exceptional (as they should be) this won’t be a problem. However, if you’re using a large number of exceptions in a tight loop it could have a negative impact on performance. I’m assuming that a catch-throw block doesn’t need to create a stack trace – let’s see if we can get a rough idea of the performance impact.
start = Time.now
10_000_000.times do |i|
begin
raise StandardError, "Error #{i}"
rescue StandardError => error
error.inspect
end
end
puts "Raise&Rescue Operation took #{Time.now - start} seconds"
start = Time.now
10_000_000.times do |i|
catch (:the_loop) do
throw :the_loop
end
end
puts "Catch&Throw Operation took #{Time.now - start} seconds"
As I mentioned, these are very rough performance estimates, but there does seem to be a significant performance difference between using exceptions and using a catch-throw block.
Conclusion
The catch-throw block seems to be a lightweight error handling mechanism – useful when you want to jump out of a nested construct during normal processing. The performance figures point to the same conclusion.
Happy coding.