Handling Exceptions in Grape for Ruby
source link: https://blog.appsignal.com/2024/04/17/handling-exceptions-in-grape-for-ruby.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Grape is a popular Ruby framework for building RESTful APIs. Exception handling plays a crucial role in ensuring the stability and reliability of any application, including those made with Grape.
This article will explore the basics of Grape exception handling, including customizing exceptions. We'll also touch on some best practices, and how to integrate your app with AppSignal for enhanced error monitoring and management.
Let's get started!
Basics of Grape Exception Handling
In this tutorial, we’ll see how to handle exceptions in a Grape API built in Rails. I have made a demo job board API for this, and you can check out the source code on GitHub.
Raising an Exception
You can raise an exception in Grape by using error!
. For example, in the job API mentioned above, we have a show
route that returns a job based on the ID
. We can return a 404
error when the record is not available, like this:
get do
if job
present job
else
error!('404 Not Found', 404)
end
end
When you raise an exception, you’ll want to handle it in a “unique” way — you most likely will not want to send the raised exception to your users.
In Ruby, we have a default mechanism for exception handling. It works by wrapping code that might raise an exception in a begin
block. The rescue
block is used to handle the exception that has been raised.
begin
#... process, may raise an exception
rescue =>
#... error handler
else
#... executes when no error
ensure
#... always executed
end
So, here is what that will look like in a typical scenario:
begin
File.readlines('input.txt').each { |line| values << Float(line) }
rescue Errno::ENOENT
p 'file not found'
rescue ArgumentError
p 'file contains unparsable numbers'
else
print values
end
The rescue_from
Method
When you raise exceptions, or they happen without your direct involvement, you’ll want to handle them properly.
By default, Grape provides a rescue_from
method. This allows you to specify a block of code that gets executed when defined exceptions are raised.
So, to “rescue” or handle the 404
error we raised before any other one that arises in the jobs
resource, we can use the rescue_from
method. The method is added above the jobs
resource.
# Rescue 404 errors
rescue_from :all do |error|
error!({ error: error.message }, 404)
end
# Jobs resource
resource :jobs do
desc 'Return list of jobs'
get do
...
end
...
end
We can also specify the content type to be used:
rescue_from :all do |error|
error!({ error: error.message }, 404, { 'Content-Type' => 'text/error' })
end
This way of handling an exception is too generic — we are rescuing every form of exception and returning an error with a 404
status code. That is misleading if our API users expect to get a 400
status code.
We can instead specify the exception we want to handle:
rescue_from ActiveRecord::RecordNotFound do |error|
error!({ error: error.message }, 404, { 'Content-Type' => 'text/error' })
end
rescue_from :all do |error|
error!({ error: error.message }, 500, { 'Content-Type' => 'text/error' })
end
When we encounter an ActiveRecord::RecordNotFound
error, we’ll return an error message with a 404
status code. Otherwise, we’ll return an error message with a 500
status code.
This shows that we can improve on what we currently have, but what if we want an error handler that rescues from all errors? That's where customizing exceptions comes in.
↓ Article continues below
Customizing Exceptions in Grape for Ruby
Depending on the type of error encountered, this error handler should be able to return an error message alongside the correct status code.
First, create a file called exceptions_handler. Then, we’ll move our current exception handlers into the file:
# frozen_string_literal: true
module V1
module ExceptionsHandler
extend ActiveSupport::Concern
included do
rescue_from ActiveRecord::RecordNotFound do |error|
error!({ error: error.message }, 404, { 'Content-Type' => 'text/error' })
end
rescue_from :all do |error|
error!({ error: error.message }, 500, { 'Content-Type' => 'text/error' })
end
end
end
end
Our ExceptionHandler
module uses ActiveSupport::Concern
, allowing us to access functionalities like included
and class_methods
. In the snippet above, we have the error handlers in the included
block, so wherever this module is included, they will be available as they’re defined.
We can go ahead and remove the error handler from the files where we had them previously. Then we can include the ExceptionsHandler module in our API entry file — api.rb:
# frozen_string_literal: true
module V1
class API < Grape::API
include ExceptionsHandler
mount V1::Jobs
end
end
Let’s create a base error class for our errors. This class will be responsible for returning the error response.
module V1
module Exceptions
class BaseError < StandardError
attr_reader :status, :message
def initialize(message: nil, status: nil)
@status = status || 500
@message = message || "Something unexpected happened."
end
def body
Rack::Response.new({ error: message }.to_json, status)
end
end
end
end
The class accepts two keyword parameters: a message
string and a status
. If none is passed, we’ll use the default.
In the body
method, we return a Rack response. By default, the rescue_from
handler must return a Rack::Response
object, call error!
, or raise an exception.
We can go ahead and make use of it in the ExceptionsHandler
:
included do
rescue_from ActiveRecord::RecordNotFound do |error|
error!({ error: error.message }, 404, { 'Content-Type' => 'text/error' })
end
rescue_from :all do |error|
Exceptions::BaseError.new(message: error.message).body
end
end
When we call the /error
endpoint, we’ll see oops
returned as the response. At this point, we can create a class for NotFound
errors.
module V1
module Exceptions
class NotFound < BaseError
def initialize(message: nil)
super(
status: 404,
message: message || "Oops, we could not find the record you are looking for."
)
end
end
end
end
The NotFound
class only accepts message
. Since it inherits from BaseError
, we need not return a Rack::Response
again.
We can go ahead and use it in the ExceptionsHandler
like this:
included do
rescue_from ActiveRecord::RecordNotFound do |error|
Exceptions::NotFound.new(message: error).body
end
rescue_from :all do |error|
Exceptions::BaseError.new(message: error.message).body
end
end
Now, if we attempt to raise an error like this manually:
raise Exceptions::NotFound.new(message: "Something unexpected happened.......")
This will work fine, but the status code will be 500
, because it returns the response in the BaseError
class (as the BaseError
class handles the error).
To fix that, we’ll need to modify the ExceptionHandler
to explicitly use the NotFound
class to handle the error instead.
So, whenever an error corresponding to ActiveRecord::RecordNotFound
and V1::Exceptions::NotFound
is encountered, use Exceptions::NotFound
. Otherwise, use Exceptions::BaseError
.
included do
rescue_from ActiveRecord::RecordNotFound do |error|
Exceptions::NotFound.new(message: error).body
end
rescue_from V1::Exceptions::NotFound do |error|
Exceptions::NotFound.new(message: error.message).body
end
rescue_from :all do |error|
Exceptions::BaseError.new(message: error.message).body
end
end
You can see that we’ll need specificrescue_from
blocks as we create more error classes. We can improve this by using case statements:
module V1
module ExceptionsHandler
extend ActiveSupport::Concern
included do
rescue_from :all do |error|
case error.class.name
when 'ActiveRecord::RecordNotFound', 'V1::Exceptions::NotFound'
Exceptions::NotFound.new(message: error.message).body
else
Exceptions::BaseError.new(message: error.message).body
end
end
end
end
end
Et voilà!
Best Practices and Tips
While there are tons of best practices that you can employ for exception handling, here are a few quick tips to follow:
- Group related exceptions: As we saw in the code above, grouping related exceptions allow us to have maintainable code. As the number of exceptions we want to handle increases, we can add them to our list.
- Use helpers like
error!
to quickly raise exceptions. This simplifies your exception handling. - Make use of exception monitoring tools like AppSignal.
AppSignal Integration: Grape for Ruby
AppSignal helps you to monitor and track errors in your applications. Integrating AppSignal with your Grape API gives you valuable insights into exceptions. This guide shows you how to integrate AppSignal with your Grape API. Whenever an error occurs in your API, you’ll see it in your AppSignal dashboard, like so:
Wrapping Up
Exception handling is a critical aspect of developing robust APIs. In this tutorial, we’ve seen how to properly handle exceptions in a Grape API. We also briefly looked at some best practices and AppSignal's integration for Grape.
Exception handling is an ongoing process — one you’ll need to improve on consistently.
Happy coding!
P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!
P.P.S. Did you know that AppSignal offers an Active Record integration? Find out more.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK