Little Shop DRY Exception Handling

Refactoring Our Exception Handling

Revisiting Our Rescue Blocks

Let’s think back to our Set List example, where we were setting up error handling for the case when a controller action is fetching a song with an invalid ID. Remember that Song.find(<id>) will throw an exception when it’s passed an invalid song ID.

def show
  begin
    render json: SongSerializer.new(Song.find(params[:id]))
  rescue ActiveRecord::RecordNotFound => exception
    render json: {
      errors: [
        {
          status: "404",
          title: exception.message
        }
      ]
    }, status: :not_found
  end
end

This looks pretty good, but it would be nice to get that presentation logic out of the controller action, and encapsulate it into an error serializer that could be used in multiple controller actions. The jsonapi serializer gem doesn’t do a good job of creating an error serializer for us, so we will handroll one. We can pass as arguments the exception we catch in our rescue block, as well as the status code we’d like to send in that JSON response.

class ErrorSerializer
  def self.format_error(exception, status)
    {
      errors: [
        {
          status: status.status_code,
          title: exception.message
        }
      ]
    }

  end
end

Now our method will look like this:

def show
  begin
    render json: SongSerializer.new(Song.find(params[:id]))
  rescue ActiveRecord::RecordNotFound => exception
    render json: ErrorSerializer(exception, "404"), status: :not_found
  end
end

This is awesome, but what if we want to rescue from the ActiveRecord::RecordNotFound exception in other controller actions? Adding a rescue block to every action that uses Song.find(params[:id]) isn’t very DRY.

That’s where the Rails rescue_from syntax comes in! rescue_from behaves as a Rails filter that creates a rescue for each action in this controller.

class Api::V1::SongsController < ApplicationController
  rescue_from ActiveRecord::RecordNotFound, with: :not_found_response

  ...
end

:not_found_response will direct Rails to invoke a method of the same name whenever ActiveRecord::RecordNotFound is raised. The Exeception will auto-magically be passed, so we need to define this method and make sure it accepts an argument.

class Api::V1::SongsController < ApplicationController
  rescue_from ActiveRecord::RecordNotFound, with: :not_found_response

  ...

  def show
    render json: SongSerializer.new(Song.find(params[:id])) 
  end

  ...

  def update
    song = Song.find(params[:id])
    song.update(Song_params)
    render json: SongSerializer.new(song)
  end
  
  def destroy
    song = Song.find(params[:id])
    song.destroy
    render json: SongSerializer.new(song)
  end

  private
 
  def not_found_response(exception)
    render json: ErrorSerializer.new(exception.message, "404"), status: :not_found
  end
end

This is much better, because now our update and destroy actions will handle that exception the same way. The show action goes back to one line, just how it was before we started this process.

We can keep our code even more DRY by rescuing from a given exception across all controllers. Try it out by moving this rescue_from and not_found_response method to the ApplicationController!

Thinking About Patterns

Take a few minutes on your own to answer the following questions:

  • What do you like about this approach?
  • What do you not like about this approach?
  • How could you use rescue in conjunction with ActiveRecord validations?

Resources

Lesson Search Results

Showing top 10 results