Being Helpful with Ruby Exceptions and Error Messages

Back | ruby, open source | 5/15/2013 |

We talk a lot about error handling in Ruby. But we rarely talk about raising errors and creating helpful error messages that are actionable. A good error should tell the developer what went wrong and what to do about it.

One library is known for its excellent error messages. Consider a typical validation error from Mongoid.

  1. #<Mongoid::Errors::Validations:
  2. Problem:
  3.   Validation of User failed.
  4. Summary:
  5.   The following errors were found: Password can't be blank
  6. Resolution:
  7.   Try persisting the document with valid data or remove the validations.>

This error describes the problem, offers a detailed summary and provides a possible resolution!

Lets implement a similar system for the ruby-enum gem that I live-coded at NYC.rb.

First, add a dependency on i18n and require "i18n". Then, create a lib/config/locales folder and an en.yml file in it. English error messages will go there. This file will need to be loaded by our library, specifically in ruby-enum.rb.

  1. require 'i18n'
  2. I18n.load_path << File.join(File.dirname(__FILE__), "config", "locales", "en.yml")

Error descriptions inside en.yml contain the problem, summary and resolution. The YAML format supports multi-lines with \_\_ and can include values from parameters using the %{name} syntax.

  1. en:
  2.   ruby:
  3.     enum:
  4.       errors:
  5.         messages:
  6.           uninitialized_constant:
  7.             message: "Uninitialized constant."
  8.             summary: "The constant %{name}::%{key} has not been defined."
  9.             resolution: "The enumerated value could not be found in class %{name}.\n
  10.             \_Use 'define' to declare it.\n
  11.             \_Example:\n
  12.             \_\_module %{name}\n
  13.             \_\_\_include Ruby::Enum\n
  14.             \_\_\_define %{key}, 'value'\n
  15.             \_\_end"

The base error class, Ruby::Enum::Errors::Base takes care of the translation. I stripped the implementation details below – the important parts is the BASE_KEY value for localized error messages and the compose_message method. Get the full implementation here and modify it for your project.

  1. module Ruby
  2.   module Enum
  3.     module Errors
  4.       class Base < StandardError
  5.  
  6.         attr_reader :problem, :summary, :resolution
  7.  
  8.         def compose_message(key, attributes = {})
  9.           @problem = create_problem(key, attributes)
  10.           @summary = create_summary(key, attributes)
  11.           @resolution = create_resolution(key, attributes)
  12.  
  13.           "\nProblem:\n  #{@problem}" +
  14.           "\nSummary:\n  #{@summary}" +
  15.           "\nResolution:\n  #{@resolution}"
  16.         end
  17.  
  18.         private
  19.  
  20.           BASE_KEY = "ruby.enum.errors.messages" #:nodoc:
  21.  
  22.           # implementation of create_problem, summary and resolution
  23.  
  24.       end
  25.     end
  26.   end
  27. end

Specific errors derive from this class.

  1. module Ruby
  2.   module Enum
  3.     module Errors
  4.       class UninitializedConstantError < Base
  5.         def initialize(attrs)
  6.           super(compose_message("uninitialized_constant", attrs))
  7.         end
  8.       end
  9.     end
  10.   end
  11. end

When raising an UninitializedConstantError, pass the values of key and name used in the en.yml file above.

  1. raise Ruby::Enum::Errors::UninitializedConstantError.new({
  2.   :name => "Class",
  3.   :key => "CONSTANT"
  4. })

Here’s the result.

  1. 1.9.3-p362 :002 > require 'ruby-enum'
  2. => true
  3. 1.9.3-p362 :003 > raise Ruby::Enum::Errors::UninitializedConstantError.new({
  4. :name => "Class",
  5. :key => "CONSTANT"
  6. })
  7.  
  8. Ruby::Enum::Errors::UninitializedConstantError:
  9. Problem:
  10.   Uninitialized constant.
  11. Summary:
  12.   The constant Class::CONSTANT has not been defined.
  13. Resolution:
  14.   The enumerated value could not be found in class Class.
  15.   Use 'define' to declare it.
  16.   Example:
  17.    module Class
  18.     include Ruby::Enum
  19.     define CONSTANT, 'value'
  20.    end

Beautiful.