Using Custom Error Messages for Cleaner Code

03 January 2011

Either because you dread 500 Error Messages or you’re on some kind of JSON kick you find yourself writing this style of ruby code:

class Group
  def join user
    if !self.permitted?(user)
      {
        :success => false,
        :message => "You don't have the proper permissions to join this group."
      }
    elsif self.member?(user)
      {
        :success => false,
        :message => "You are already a member of this group."
      }
    else
      self.members.create :user => user
      { :success => true, :message => "Welcome to the Group!" }
    end
  end
end

class GroupsController < ApplicationController
  def join
    @group = Group.find(params[:id])
    response = @group.join(current_user)
    if response[:success]
      flash[:notice] = response[:message]
      redirect_to @group
    else
      flash[:error] = response[:message]
      render :action => 'request'
    end
  end
end

Why? Well because you don’t want to risk throwing a Ruby Exception when it could cause a 500 Error Message for a user, and you want to be able to control error messages from your Model. For example, the join method could return any one of the following responses:

{ :success => false, :message => "You are already a member of this group." }
{
  :success => false,
  :message => "You don't have the proper permissions to join this group."
}
{ :success => true, :message => "Welcome to the Group!." }

So what’s wrong with that? Well, there are a couple of things.

You introduced conditionals into your model and controller that are entirely unnecessary. Conditionals are not the worst thing in the world, but they create divergent code paths, they make your code harder to understand and harder to test. If you can avoid them, it’s easier for the next programmer to figure out what you had in mind.

You created an additional dependency between your model and controller code. Dependencies lead to bad things. The more modular your code is the easier it is to test and the more versatile it is. It’s easier to use modular code in background jobs and other objects and functions.

Thirdly, and most importantly, you are replicating a perfectly good programming concept known as Exceptions. Exceptions are a program’s way of saying: something went wrong - something is not as it should be. Any they are great to use because they are easy to read and they break the normal code flow, which is what you would expect if something went wrong.

How do we fix this code? Simple: we use Exceptions.

  1. Create the custom exceptions.
    class Group
      module Error
        class Standard < StandardError; end
    
        class AlreadyAMember < Standard
          def message
            "You are already a member of this group."
          end
        end
    
        class NotPermittedToJoin < Standard
          def message
            "You don't have the proper permissions to join this group."
          end
        end
      end
    end
  2. Raise the exceptions.
    class Group
      def join user
        raise Error::NotPermittedToJoin unless self.permitted?(user)
        raise Error::AlreadyAMember if self.member?(user)
        self.members.create :user => user
      end
    end
  3. Catch the exceptions.
    class GroupsController < ApplicationController
      def join
        @group = Group.find(params[:id])
        @group.join current_user
        flash[:notice] = "Welcome to the Group!"
        redirect_to @group
      rescue Group::Error::Standard => exception
        flash[:error] = exception.message
        render :action => 'request'
      end
    end

A gist of this code can be found here.