DEV Community

Martin Souza
Martin Souza

Posted on

Rescue Me: Techniques for Active Record Error Handling in Ruby on Rails

Active Record provides many mighty tools to streamline the work of building a backend API, including for when things go wrong. In this post, we'll walk through three error-handling techniques, getting more sophisticated and powerful as we go.

For the sake of simplicity, our examples will deal with failing to find a record in our database, which we'll illustrate in the context of a standard, RESTful show controller action. However, these techniques are applicable to other types of errors as well.

Basic Error-Handling with Conditional Logic

The simplest way to handle Active Record errors is just to use conditional logic in your controller actions to specify what your API should do if an error arises. We might write our show action like this:

def show
  dingus = Dingus.find_by(id: params[:id])
  if dingus?
    render json: dingus, status: :ok
  else
    render json: {error: "Dingus not found"}, status: :not_found
  end
end
Enter fullscreen mode Exit fullscreen mode

Let's break down what's happening here. First, we initialize a variable to refer to the Dingus record we want from the database, and we use find_by to retrieve it with the id provided by the params hash. (Read more about the params hash here.) Then, the conditional statement if dingus? checks whether that variable has a truthy or falsey value. This works because if the find_by operation locates a record and assigns it to the variable, dingus is truthy. If no record is found, the value of dingus is nil, which is falsey. Accordingly, if the condition is met, the API sends the data in its response, along with the successful :ok status code. Otherwise, it sends an error message, which we've produced manually as a simple hash, along with the appropriate :not_found status.

Handling errors like this has certain advantages. For one thing, this approach is easy to understand, and all the code for it exists in one place. It gives you very specific control over how an individual controller action responds to a certain error, and for that reason this technique can sometimes be useful even if you're using other more sophisticated methods elsewhere—particularly if you're creating custom controller actions.

However, for common errors, this technique is inefficient and clunky. You don't want to have to write separate if/else logic everywhere, and you definitely shouldn't compose your own error messages for every single case.

Basic Error-Handling with Rescue Blocks

Fortunately, Active Record gives us more tools and a better way to do things. In our first example, we used a simple logical condition to check whether or not our show action found a record, but we can take advantage of some built-in functionality instead.

Certain Active Record methods return instances of special classes called exceptions in the event of failure. For instance, the find method takes an id and returns a RecordNotFound exception if there's no record with a matching id attribute. It is less flexible than the find_by method in our first example, which can search using whatever attribute we like, but returning an exception instead of just nil when it doesn't find a matching record is a major advantage. For one thing, exceptions come with their own error messages, which we can access and send to the frontend rather than having to compose them ourselves. More importantly, they also enable us to use rescue blocks to define how we want to handle errors.

The rescue keyword is kind of like special conditional logic that looks out for exceptions of the specified type. When the right kind of exception occurs, the code block runs, doing whatever we've written to deal with the error.

Let's rewrite our show action to use a rescue block:

def show
  render json: Dingus.find(params[:id]), status: :ok
rescue ActiveRecord::RecordNotFound
  render json: {error: "Dingus not found"}, status: :not_found
end
Enter fullscreen mode Exit fullscreen mode

Now we're using find to retrieve the requested record, and simply sending it to the frontend without assigning it to a variable first (though you can certainly still do so, and might have reason to in certain cases). If no record exists with a matching id, find returns an ActiveRecord::RecordNotFound exception—and when that happens, our rescue block kicks in and runs the same code we used in the else part of our first example.

This is a step in the right direction, but we still have to write a rescue block for each controller action. Maybe that's fine if we want to deal with a certain kind of exception for a single action, but it's annoying if we want to handle the same kinds of errors in multiple places.

Intermediate Error-Handling with rescue_from

Fortunately, we can entirely separate our error-handling from our controller actions. Just like rescue, rescue_from defines a response to a specific type of exception. However, rather than being attached to a particular action, a rescue_from is independently defined, and applies any time the specified exception occurs, no matter what action it comes from.

With a rescue_from, our whole example controller now looks like this:

class DingusesController < ApplicationController
  rescue_from ActiveRecord::RecordNotFound, with: :render_not_found

  def show
    render json: Dingus.find(params[:id]), status: :ok
  end

  private
  def render_not_found
    render json: {error: "Dingus not found"}, status: :not_found
  end
end
Enter fullscreen mode Exit fullscreen mode

Now the show action doesn't need to include anything concerned with potential errors. We've refactored the error response into a separate method called render_not_found, which our rescue_from will use whenever a RecordNotFound exception occurs.

At this point, since our error-handling is separated from our controller actions, we can make it even more generally performant by relocating it to our top-level controller, like so:

class ApplicationController < ActionController::API
  rescue_from ActiveRecord::RecordNotFound, with: :render_not_found

  private
  def render_not_found(exception)
    render json: {exception.model: "Not found"}, status: :not_found
  end
end
Enter fullscreen mode Exit fullscreen mode

Since DingusesController inherits from ApplicationController, it will use the rescue_from defined there—as will all our other controllers. We've also adjusted the private render_not_found method in order to generalize and reuse our error response whenever a RecordNotFound exception occurs anywhere. Now it takes the exception instance as a parameter so we can call exception.model to get whatever kind of resource wasn't found. In our example case, this would give us {Dingus: "Not found"}.

TLDR

The best approach for handling most Active Record errors in to use rescue_from in your top-level application controller. It's a bit more abstract, but it's the most effective and efficient way to cover common types of errors for multiple database resources. Once you understand how to use rescue_from, there's probably little reason to ever use an individual rescue block.

However, there may be still be times where good old if/else logic can provide a good one-off solution to deal with errors in special cases.

Top comments (0)