Actions: Exception Handling


Actions have an elegant API for exception handling. The behavior changes according to the current Hanami environment and the custom settings in our configuration.

Default Behavior

# apps/web/controllers/dashboard/index.rb
module Web
  module Controllers
    module Dashboard
      class Index
        include Web::Action

        def call(params)
          raise 'boom'
        end
      end
    end
  end
end

Exceptions are automatically caught when in production mode, but not in development. In production, for our example, the application returns a 500 (Internal Server Error); in development, we’ll see the stack trace and all the information to debug the code.

This behavior can be changed with the handle_exceptions setting in apps/web/application.rb.

Custom HTTP Status

If we want to map an exception to a specific HTTP status code, we can use handle_exception DSL.

# apps/web/controllers/dashboard/index.rb
module Web
  module Controllers
    module Dashboard
      class Index
        include Web::Action
        handle_exception ArgumentError => 400

        def call(params)
          raise ArgumentError
        end
      end
    end
  end
end

handle_exception accepts a Hash where the key is the exception to handle, and the value is the corresponding HTTP status code. In our example, when ArgumentError is raised, it will be handled as a 400 (Bad Request).

Custom Handlers

If the mapping with a custom HTTP status doesn’t fit our needs, we can specify a custom handler and manage the exception by ourselves.

# apps/web/controllers/dashboard/index.rb
module Web
  module Controllers
    module Dashboard
      class PermissionDenied < StandardError
        def initialize(role)
          super "You must be admin, but you are: #{ role }"
        end
      end

      class Index
        include Web::Action
        handle_exception PermissionDenied => :handle_permission_error

        def call(params)
          unless current_user.admin?
            raise PermissionDenied.new(current_user.role)
          end

          # ...
        end

        private
        def handle_permission_error(exception)
          status 403, exception.message
        end
      end
    end
  end
end

If we specify the name of a method (as a symbol) as the value for handle_exception, this method will be used to respond to the exception. In the example above we want to protect the action from unwanted access: only admins are allowed.

When a PermissionDenied exception is raised it will be handled by :handle_permission_error. It MUST accept an exception argument—the exception instance raised inside #call.

When specifying a custom exception handler, it MUST accept an exception argument.