How to tackle exceptions in registered components? (dry-container)

I like my services to define their own inner exceptions like this:

class Service
  InvalidState      = Class.new(StandardError)
  InvalidTransition = Class.new(StandardError)

  def call
    #...
  end
end

So that you easily see what you rescue:

begin
  Service.new.call
rescue Service::InvalidState
  #...
end

Now, if I what to move Service to the container, how should the usage look like? I see some possibilites, none being fully satisfying:

begin
  main_container['service'].new.call
rescue main_container['service']::InvalidState
  #...
end

you need to initialize the service outside the cointainer

begin
  main_container['service'].call
rescue Service::InvalidState
  #...
end

you hardcode the service’s name, therefore lose the advantage of containers

begin
  main_container['service']
rescue main_container['errors.invalidstate']
  #...
end

you lose the composition

Are there any good practises or advice?

If I have a service with several implementations, I define interface as a module or abstract class, and expect each of concrete implementations must implement this interface. Thus, public interface consist of method signatures and exceptions.

For example, thats how interface for sending SMS’s may looks like:

# @abstract subclass and define +send_sms+ method
module SmsDispatcher
  DeliveryError = Class.new(StandardError)
  TimeoutError = Class.new(DeliveryError)
  ExternalServiceError = Class.new(DeliveryError)

  # @param msisdn [String] with country code
  # @param message [String]
  # @return [String, nil] message id in external system
  # @raise [TimeoutError] when recoverable error happens
  # @raise [ExternalServiceError] when external service failed to execute request
  # @examlpe 
  #    dispatcher = SMPPDispatcher.new(config)
  #    dispatcher.send_sms('+1777777777', 'Hi, how are you?')
  #
  def send_sms(msisdn, message)
  end
end

So, I don’t depend on unknown service. I depend on SmsDispatcher interface. To emphasise this, you may register dependency using the name of the interface. Like this:

register('SmsDispatcher') { SMPPDispatcher.new }