Search code examples
rubyerror-handlinghanami

Best practice of error handling on controller and interactor


# users_show_controller.rb
class Controllers::Users::Show
  include Hanami::Action

  params do
    required(:id).filled(:str?)
  end

  def call(params)
    result = users_show_interactor(id: params[:id])

    halt 404 if result.failure?
    @user = result.user
  end
end

# users_show_interactor.rb
class Users::Show::Interactor
  include Hanami::Interactor

  expose :user
  def call(:id)
    @user = UserRepository.find_by(:id)
  end
end

I have a controller and a interactor like above. And I'm considering the better way to distinguish ClientError from ServerError, on the controller.

I think It is nice if I could handle an error like below.

handle_exeption StandardError => :some_handler

But, hanami-interactor wraps errors raised inside themselves and so, controller receive errors through result object from interactor.

I don't think that re-raising an error on the controller is good way.

result = some_interactor.call(params)
raise result.error if result.failure

How about implementing the error handler like this? I know the if statement will increase easily and so this way is not smart.

def call(params)
  result = some_interactor.call(params)
  handle_error(result.error) if result.faulure?
end

private

def handle_error(error)
  return handle_client_error(error) if error.is_a?(ClientError)
  return server_error(error) if error.is_a?(ServerError)
end

Solution

  • Not actually hanami-oriented way, but please have a look at dry-monads with do notation. The basic idea is that you can write the interactor-like processing code in the following way

    def some_action
      value_1 = yield step_1
      value_2 = yield step_2(value_1)
      return yield(step_3(value_2))
    end 
    
    def step_1
      if condition
        Success(some_value)
      else
        Failure(:some_error_code)
      end
    end
    
    def step_2
      if condition
        Success(some_value)
      else
        Failure(:some_error_code_2)
      end
    end
    

    Then in the controller you can match the failures using dry-matcher:

    matcher.(result) do |m|
      m.success do |v|
        # ok
      end
    
      m.failure :some_error_code do |v|
        halt 400
      end
    
      m.failure :some_error_2 do |v|
        halt 422
      end
    end
    

    The matcher may be defined in the prepend code for all controllers, so it's easy to remove the code duplication.