Notification

Replacing exceptions with Notification in validations

A common way to approach validation is to run series of checks on some data. If any of these checks fails, you raise an exception with an error message.
I have a couple of problems with this approach. Firstly I'm not happy with using exceptions for something like this. Exceptions signal something outside the expected bounds of behavior of the code in question. But if you're running some checks on outside input, this is because you expect some messages to fail - and if a failure is expected behavior, then you shouldn't be using exceptions.
The second problem with code like this is that it fails with the first error it detects, but usually it's better to report all errors with the incoming data, not just the first. That way a client can choose to display all errors for the user to fix in a single interaction.

My preferred way to deal with reporting validation issues like this is the Notification pattern. A notification is an object that collects errors, each validation failure adds an error to the notification. A validation method returns a notification, which you can then interrogate to get more information.

When to use this refactoring

Exceptions are a very useful technique for handling exceptional behaviour and getting it away from the main flow of logic. This refactoring is a good one to use only when the outcome signaled by the exception isn't really exceptional, and thus should be handled through the main logic of the program. The example I'm looking at here, validation, is a common case of that.
Whether to use exceptions for a particular task is dependent on the context. So, as the prags go on to say, reading from a file that isn't there may or may not be an exception depending on the circumstances. If you are trying to read a well known file location, such as /etc/hosts on a unix system, then it's likely you can assume the file should be there, so throwing an exception is reasonable. On the other hand if you are trying to read a file from a path that the user has typed in on the command-line, then you should expect that it's likely the file isn't there, and should use another mechanism - one that communicates the unexceptional nature of the error.

There is a case when it may be sensible to use exceptions for validation failures. This would be situations where you have data that you expect to have already been validated earlier in processing, but you want to run the validation checks again to guard against a programming error letting some invalid data slip through.

Example

require 'date'

class BookingRequest
  def initialize(number_of_seats:, date:)
    @number_of_seats = number_of_seats
    @date            = date
  end

  def check
    raise ArgumentError, validation.error_message if validation.has_errors?
  end

  def validation
    note = Notification.new
    validate_date note
    validate_number_of_seats note
    note
  end

  private

  def validate_date(note)
    return note.add_error 'date is missing' if date.nil?
    
    begin
      parsed_date = Date.parse date
    rescue Exception => exp
      note.add_error 'Invalid format for date', exp
      return
    end

    note.add_error 'date cannot be before today' if parsed_date < Date.today
  end

  def validate_number_of_seats(note)
    return note.add_error 'number of seats cannot be null' if number_of_seats.nil? 
    note.add_error 'number of seats must be positive' if number_of_seats < 1
  end

  attr_reader :number_of_seats, :date
end
require 'set'

class Notification
  def initialize
    @errors = Set.new
  end

  def add_error(message)
    add_error message, nil
  end

  def add_error(message, exception)
    @errors.add Error.new(message, exception)
  end

  def has_errors?
    !@errors.empty?
  end

  def error_message
    @errors.map(&:message).join(', ')
  end

  private

  class Error
    def initialize(message:, cause:)
      @message = message
      @cause   = cause
    end
  end
end
Alexander M Almost 8 years ago