107

Is there a way to catch all uncatched exceptions in a rails controller, like this:

def delete
  schedule_id = params[:scheduleId]
  begin
    Schedules.delete(schedule_id)
  rescue ActiveRecord::RecordNotFound
    render :json => "record not found"
  rescue ActiveRecord::CatchAll
    #Only comes in here if nothing else catches the error
  end
  render :json => "ok"
end

Thank you

Promise Preston
  • 24,334
  • 12
  • 145
  • 143
Neigaard
  • 3,726
  • 9
  • 49
  • 85

6 Answers6

213

You can also define a rescue_from method.

class ApplicationController < ActionController::Base
  rescue_from ActionController::RoutingError, :with => :error_render_method

  def error_render_method
    respond_to do |type|
      type.xml { render :template => "errors/error_404", :status => 404 }
      type.all  { render :nothing => true, :status => 404 }
    end
    true
  end
end

Depending on what your goal is, you may also want to consider NOT handling exceptions on a per-controller basis. Instead, use something like the exception_handler gem to manage responses to exceptions consistently. As a bonus, this approach will also handle exceptions that occur at the middleware layer, like request parsing or database connection errors that your application does not see. The exception_notifier gem might also be of interest.

Jin Hian Lee
  • 173
  • 1
  • 4
BOFH
  • 2,159
  • 2
  • 12
  • 5
  • 5
    This is even more handy as it allows to catch exceptions in a DRY manner. – m33lky May 07 '12 at 19:52
  • And if i use rescue_from with no params ? will that behave the same as rescue ? catch all errors ? – minohimself Jan 18 '14 at 22:07
  • 3
    Isn't it bad practice to `rescue_from Exception`? My understanding is that it is better to rescue from `StandardError`, so things like `SyntaxError` and `LoadError` are not caught. – lobati Aug 22 '14 at 18:55
  • Yes, it is bad form to rescue 'Exception'. See Avdi Grimm's "Exceptional Ruby" for the reasons why that can be problematic. – Midwire May 26 '15 at 16:01
113
begin
  # do something dodgy
rescue ActiveRecord::RecordNotFound
  # handle not found error
rescue ActiveRecord::ActiveRecordError
  # handle other ActiveRecord errors
rescue # StandardError
  # handle most other errors
rescue Exception
  # handle everything else
  raise
end
Robin Daugherty
  • 7,115
  • 4
  • 45
  • 59
Chris Johnsen
  • 214,407
  • 26
  • 209
  • 186
  • 46
    Isn't the rule to NEVER catch Exception? – RonLugge Nov 18 '14 at 01:13
  • 2
    but how can I catch all type in `rescue => e` block only? – Matrix Feb 19 '15 at 15:03
  • 12
    @RonLugge it depends entirely on the situation at hand. applying "never" as a rule of thumb is a bad idea. – Justin Skiles Feb 25 '15 at 00:04
  • 11
    @JustinSkiles Catching Exception will catch syntax errors (and interrupt signals too). Give me one good scenario for doing that in production code. Catching signals directly I can get, but you'd need to do so explicitly to make it clear you're creating a signal handler. Just catching Exception... bad, bad idea. Catches even the things you shouldn't try to catch. – RonLugge Feb 25 '15 at 22:27
  • 1
    You can use `rescue => e` which would catch all `StandardError` errors but not the other errors like signals, etc...See Ruby Exception hierarchy: http://rubylearning.com/satishtalim/ruby_exceptions.html – Aldo 'xoen' Giambelluca Oct 17 '16 at 13:10
  • 7
    One of the few common cases where it’s sane to rescue from Exception is for logging/reporting purposes, in which case you should immediately re-raise the exception: http://stackoverflow.com/a/10048406/252346 – aelesbao Mar 30 '17 at 10:21
40

You can catch exceptions by type:

rescue_from ::ActiveRecord::RecordNotFound, with: :record_not_found
rescue_from ::NameError, with: :error_occurred
rescue_from ::ActionController::RoutingError, with: :error_occurred
# Don't resuce from Exception as it will resuce from everything as mentioned here "http://stackoverflow.com/questions/10048173/why-is-it-bad-style-to-rescue-exception-e-in-ruby" Thanks for @Thibaut Barrère for mention that
# rescue_from ::Exception, with: :error_occurred 

protected

def record_not_found(exception)
  render json: {error: exception.message}.to_json, status: 404
  return
end

def error_occurred(exception)
  render json: {error: exception.message}.to_json, status: 500
  return
end
mohamed-ibrahim
  • 10,837
  • 4
  • 39
  • 51
  • 2
    Careful not to rescue from `Exception` directly; see http://stackoverflow.com/questions/10048173/why-is-it-bad-style-to-rescue-exception-e-in-ruby – Thibaut Barrère Jan 20 '16 at 16:15
  • Thanks for this answer! It is worth nothing that `rescue_from ::ActionController::RoutingError, with: :error_occurred` does not work though – Islam Azab Sep 05 '22 at 03:04
11

rescue with no arguments will rescue any error.

So, you'll want:

def delete
  schedule_id = params[:scheduleId]
  begin
    Schedules.delete(schedule_id)
  rescue ActiveRecord::RecordNotFound
    render :json => "record not found"
  rescue
    #Only comes in here if nothing else catches the error
  end
  render :json => "ok"
end
PreciousBodilyFluids
  • 11,881
  • 3
  • 37
  • 44
  • 11
    Stale question, but this answer is incorrect. rescue without argument handles only StandardError http://robots.thoughtbot.com/rescue-standarderror-not-exception – Keith Gaddis Mar 20 '14 at 16:24
2

Error handling for a nicer user experience is a very tough thing to pull off correctly.

Here I have provided a fully-complete template to make your life easier. This is better than a gem because its fully customizable to your application.

Note: You can view the latest version of this template at any time on my website: https://westonganger.com/posts/how-to-properly-implement-error-exception-handling-for-your-rails-controllers

Controller

class ApplicationController < ActiveRecord::Base

  def is_admin_path?
    request.path.split("/").reject{|x| x.blank?}.first == 'admin'
  end

  private
  
  def send_error_report(exception, sanitized_status_number)
    val = true

    # if sanitized_status_number == 404
    #   val = false
    # end

    # if exception.class == ActionController::InvalidAuthenticityToken
    #   val = false
    # end

    return val
  end

  def get_exception_status_number(exception)
    status_number = 500

    error_classes_404 = [
      ActiveRecord::RecordNotFound,
      ActionController::RoutingError,
    ]

    if error_classes_404.include?(exception.class)
      if current_user
        status_number = 500
      else
        status_number = 404
      end
    end

    return status_number.to_i
  end

  def perform_error_redirect(exception, error_message:)
    status_number = get_exception_status_number(exception)

    if send_error_report(exception, status_number)
      ExceptionNotifier.notify_exception(exception, data: {status: status_number})
    end

    ### Log Error
    logger.error exception

    exception.backtrace.each do |line| 
      logger.error line
    end

    if Rails.env.development?
      ### To allow for the our development debugging tools
      raise exception
    end

    ### Handle XHR Requests
    if (request.format.html? && request.xhr?)
      render template: "/errors/#{status_number}.html.erb", status: status_number
      return
    end

    if status_number == 404
      if request.format.html?
        if request.get?
          render template: "/errors/#{status_number}.html.erb", status: status_number
          return
        else
          redirect_to "/#{status_number}"
        end
      else
        head status_number
      end

      return
    end

    ### Determine URL
    if request.referrer.present?
      url = request.referrer
    else
      if current_user && is_admin_path? && request.path.gsub("/","") != admin_root_path.gsub("/","")
        url = admin_root_path
      elsif request.path != "/"
        url = "/"
      else
        if request.format.html?
          if request.get?
            render template: "/errors/500.html.erb", status: 500
          else
            redirect_to "/500"
          end
        else
          head 500
        end

        return
      end
    end

    flash_message = error_message

    ### Handle Redirect Based on Request Format
    if request.format.html?
      redirect_to url, alert: flash_message
    elsif request.format.js?
      flash[:alert] = flash_message
      flash.keep(:alert)

      render js: "window.location = '#{url}';"
    else
      head status_number
    end
  end

  rescue_from Exception do |exception|
    perform_error_redirect(exception, error_message: I18n.t('errors.system.general'))
  end

end

Testing

To test this in your specs you can use the following template:

feature 'Error Handling', type: :controller do

  ### Create anonymous controller, the anonymous controller will inherit from stated controller
  controller(ApplicationController) do
    def raise_500
      raise Errors::InvalidBehaviour.new("foobar")
    end

    def raise_possible_404
      raise ActiveRecord::RecordNotFound
    end
  end

  before(:all) do
    @user = User.first

    @error_500 = I18n.t('errors.system.general')
    @error_404 = I18n.t('errors.system.not_found')
  end

  after(:all) do
    Rails.application.reload_routes!
  end

  before :each do
    ### draw routes required for non-CRUD actions
    routes.draw do
      get '/anonymous/raise_500'
      get '/anonymous/raise_possible_404'
    end
  end

  describe "General Errors" do

    context "Request Format: 'html'" do
      scenario 'xhr request' do
        get :raise_500, format: :html, xhr: true
        expect(response).to render_template('errors/500.html.erb')
      end

      scenario 'with referrer' do
        path = "/foobar"

        request.env["HTTP_REFERER"] = path

        get :raise_500
        expect(response).to redirect_to(path)

        post :raise_500
        expect(response).to redirect_to(path)
      end

      scenario 'admin sub page' do
        sign_in @user

        request.path_info = "/admin/foobar"

        get :raise_500
        expect(response).to redirect_to(admin_root_path)

        post :raise_500
        expect(response).to redirect_to(admin_root_path)
      end

      scenario "admin root" do
        sign_in @user

        request.path_info = "/admin"

        get :raise_500
        expect(response).to redirect_to("/")

        post :raise_500
        expect(response).to redirect_to("/")
      end

      scenario 'public sub-page' do
        get :raise_500
        expect(response).to redirect_to("/")

        post :raise_500
        expect(response).to redirect_to("/")
      end

      scenario 'public root' do
        request.path_info = "/"

        get :raise_500
        expect(response).to render_template('errors/500.html.erb')
        expect(response).to have_http_status(500)

        post :raise_500
        expect(response).to redirect_to("/500")
      end

      scenario '404 error' do
        get :raise_possible_404
        expect(response).to render_template('errors/404.html.erb')
        expect(response).to have_http_status(404)

        post :raise_possible_404
        expect(response).to redirect_to('/404')

        sign_in @user

        get :raise_possible_404
        expect(response).to redirect_to('/')

        post :raise_possible_404
        expect(response).to redirect_to('/')
      end
    end

    context "Request Format: 'js'" do
      render_views ### Enable this to actually render views if you need to validate contents
      
      scenario 'xhr request' do
        get :raise_500, format: :js, xhr: true
        expect(response.body).to include("window.location = '/';")

        post :raise_500, format: :js, xhr: true
        expect(response.body).to include("window.location = '/';")
      end

      scenario 'with referrer' do
        path = "/foobar"

        request.env["HTTP_REFERER"] = path

        get :raise_500, format: :js
        expect(response.body).to include("window.location = '#{path}';")

        post :raise_500, format: :js
        expect(response.body).to include("window.location = '#{path}';")
      end

      scenario 'admin sub page' do
        sign_in @user

        request.path_info = "/admin/foobar"

        get :raise_500, format: :js
        expect(response.body).to include("window.location = '#{admin_root_path}';")

        post :raise_500, format: :js
        expect(response.body).to include("window.location = '#{admin_root_path}';")
      end

      scenario "admin root" do
        sign_in @user

        request.path_info = "/admin"

        get :raise_500, format: :js
        expect(response.body).to include("window.location = '/';")

        post :raise_500, format: :js
        expect(response.body).to include("window.location = '/';")
      end

      scenario 'public page' do
        get :raise_500, format: :js
        expect(response.body).to include("window.location = '/';")

        post :raise_500, format: :js
        expect(response.body).to include("window.location = '/';")
      end

      scenario 'public root' do
        request.path_info = "/"

        get :raise_500, format: :js
        expect(response).to have_http_status(500)

        post :raise_500, format: :js
        expect(response).to have_http_status(500)
      end

      scenario '404 error' do
        get :raise_possible_404, format: :js
        expect(response).to have_http_status(404)

        post :raise_possible_404, format: :js
        expect(response).to have_http_status(404)

        sign_in @user

        get :raise_possible_404, format: :js
        expect(response).to have_http_status(200)
        expect(response.body).to include("window.location = '/';")

        post :raise_possible_404, format: :js
        expect(response).to have_http_status(200)
        expect(response.body).to include("window.location = '/';")
      end
    end

    context "Other Request Format" do
      scenario '500 error' do
        get :raise_500, format: :json
        expect(response).to have_http_status(500)

        post :raise_500, format: :json
        expect(response).to have_http_status(500)
      end
      
      scenario '404 error' do
        get :raise_possible_404, format: :json
        expect(response).to have_http_status(404)

        post :raise_possible_404, format: :json
        expect(response).to have_http_status(404)

        sign_in @user

        get :raise_possible_404, format: :json
        expect(response).to have_http_status(500)

        post :raise_possible_404, format: :json
        expect(response).to have_http_status(500)
      end
    end

  end

end
Weston Ganger
  • 6,324
  • 4
  • 41
  • 39
1

Actually, if you really want to catch everything, you just create your own exceptions app, which let's you customize the behavior that is usually handled by the PublicExceptions middleware: https://github.com/rails/rails/blob/4-2-stable/actionpack/lib/action_dispatch/middleware/public_exceptions.rb

A bunch of the other answers share gems that do this for you, but there's really no reason you can't just look at them and do it yourself.

A caveat: make sure you never raise an exception in your exception handler. Otherwise you get an ugly FAILSAFE_RESPONSE https://github.com/rails/rails/blob/4-2-stable/actionpack/lib/action_dispatch/middleware/show_exceptions.rb#L4-L22

BTW, the behavior in the controller comes from rescuable: https://github.com/rails/rails/blob/4-2-stable/activesupport/lib/active_support/rescuable.rb#L32-L51

BF4
  • 1,076
  • 7
  • 23