Ruby

Handling Exceptions in Grape for Ruby

Handling Exceptions in Grape for Ruby

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:

Ruby
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.

Ruby
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:

Ruby
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.

Ruby
# 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:

Ruby
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:

Ruby
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.

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:

Ruby
# 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:

Ruby
# 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.

Ruby
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:

Ruby
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.

Ruby
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:

Ruby
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:

Ruby
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.

Ruby
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_fromblocks as we create more error classes. We can improve this by using case statements:

Ruby
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:

  1. 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.
  2. Use helpers like error! to quickly raise exceptions. This simplifies your exception handling.
  3. 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:

AppSignal Dashboard

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.

Published

Wondering what you can do next?

  • Share this article on social media
Kingsley Chijioke

Kingsley Chijioke

Our guest author Kingsley is a Software Engineer who enjoys writing technical articles.

All articles by Kingsley Chijioke

Become our next author!

Find out more
$appsignal install

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal