RenderReturn: a controller Exception
Easily end a controller action without risking the double render error
Skip to tl;dr
AbstractController::DoubleRenderError
Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like "redirect_to(...) and return".
If you’ve been around the block with Rails, you’ve probably seen this error. The error message is sufficiently explanatory: don’t call render
and/or redirect_to
multiple times. Often this happens because you tried to extract a redirect_to
call into some private method and failed to completely exit out of the controller action.
Here, in this message, Rails itself is suggesting violating the Ruby Style Guide by suggesting redirect_to(...) and return
. Now, I violate this aspect of the style guide myself: I will never give up construct such as x = find_the_thing or raise (...)
. But here it’s a little more sinful: it implicitly relies on the return value of redirect_to
being truthy. The docs don’t even specify what the return value of redirect_to
should be. This is a bad pattern to follow.
Moreover, this doesn’t even solve the nested-method problem I alluded to above:
class MyController < ActionController::Base
def show
check_for_bad_stuff
# ...
render json: the_data
end
private
def check_for_bad_stuff
redirect_to :error_page if bad_condition?
log "Checked the thing"
end
end
redirect_to and return
won’t work here for obvious reasons: the return
simply goes back to the controller. So you could bubble the return
up to the show
action: check_for_bad_stuff and return
. But this requires different return values in check_for_bad_stuff
. Ok, let’s bite:
def check_for_bad_stuff
if bad_condition?
log "Nope."
redirect_to :error_page
return false
else
log "It checks out."
return true
end
end
Except it’s now it’s redirect_to(...) or return
. Fine. It all makes sense, and is easy to follow when it’s the only thing you’re looking at, but this simple concept of “redirect and get out of here” has already taken up for more of our attention than it deserves.
Other similarly unconvincing blogs give a short list of working but frankly similarly bad or, worse, a touch cryptic, solutions.
So here’s mine.
This, to me, screams out as a use case for raise
, coupled with rescue_from
. That is, “get out of here completely, no matter how buried down the stack you are.” Ultimately, that’s what you want after some of these redirect_to
s. I’ll briefly mention throw :halt
works too, as I learned from a comment in the above linked blog, but that’s not as of yet well documented Rails behavior (read: not guaranteed to work), and frankly I dislike the potential for naming conflicts with throw
. Exceptions work perfectly well in this case.
class ApplicationController < ActionController::Base
rescue_from RenderReturnException, with: :render_return
def render_return
# Do nothing.
end
end
Now,
def check_for_bad_stuff
if bad_condition?
log "Nope."
redirect_to :error_page
raise RenderReturnException
end
log "It checks out."
end
The RenderReturnException
will immediately halt the action, and the rescue_from
catches (to use Java and distinctly non-Ruby terminology) that exception in a method that does nothing so that you don’t get any other error handling such as Rails’ standard 500 error.
There are no implicit return values to keep track of, no boolean flipping, no hidden bugs just because you extracted code into a different method. You have only a single convention added to your code toolbelt to learn: RenderReturnException
is a safe exception to throw to stop a controller action. Which is great, because now that’s a tool you can use throughout your controllers.
If you wanted to, you could make it even more explicitly named using a method called, say halt_controller_action
, which you can call by name anywhere in your controllers:
class ApplicationController < ActionController::Base
rescue_from RenderReturnException, with: :render_return
def render_return
# Do nothing.
end
def halt_controller_action
raise RenderReturnException
end
end
Wasn’t that simple?