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
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
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
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
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)