Using throw and catch to tidy up our code

Let’s say we want a simple controller action that checks, if a given code is valid. It should return true and false and, if the code is invalid, give the reason (whether that is because it is unknown or because it has been used already). The action could look like this:

def check
  code = Code.find_by_value(params[:code])
  if code
    if code.used?
      respond_with do |format|
        format.json { render json: {valid: false, reason: "used"} }
      end
    else
      respond_with do |format|
        format.json { render json: {valid: true} }
      end
    end
  else
    respond_with do |format|
      format.json { render json: {valid: false, reason: "unknown"} }
    end
  end
end

Now this deep nesting effectively hides the underlying algorithm, a simple one in this case. Expressing this using throw and catch straightens the code a bit:

def check
  result = catch(:result) do
    code = Code.find_by_value(params[:code])
    throw :result, {valid: false, reason: "unknown"} unless code
    throw :result, {valid: false, reason: "used"} if code.used?
    {valid: true}
  end
  respond_with do |format|
    format.json { render json: result }
  end
end

We are down to one respond_with line but the hash merging and the throws at the beginning of the line are not particularly pretty.

Let’s introduce a little Ruby mixin for the Hash class that provides it with a compact method that its friend Array has had all along:

class Hash
  def compact
    delete_if { |k, v| v.nil? }
  end
end

Now using this to clean up the response hash and moving the throw statements to the end so the steps of the algorithm are visible we have our final version of the action:

def check
  result = catch(:result) do
    code = Code.find_by_value(params[:code])
    throw :result, reason: "unknown" unless code
    throw :result, reason: "used" if code.used?
    {valid: true}
  end
  respond_with do |format|
    format.json { render json: {valid: result[:reason].nil?, reason: result[:reason]}.compact }
  end
end