All Futures supports the same error handling pipeline as Active Record, errors, which is an enumerable instance of ActiveModel::Errors.

When validations fail, ActiveModel::Error instances will be added to the errors.objects Array. objects is actually just an alias for errors.errors, which is just not pretty enough for Rails. 🙈

In the interest of brevity and readability, the receiver and errors object have been omitted from every reference to a method in this chapter.

When you read full_messages_for :name, it is a stand-in for record.errors.full_messages_for :name.

When a model is initialized, its objects Array is empty until you either call save / update or invoke valid? directly. valid? returns false if at least one validation failed, and there will now be at least one ActiveModel::Error in the objects Array.

Successful failure

The valid? method clears the objects Array, which means that adding errors will not make a model invalid. Instead, it's failed validations that typically add the errors.

class Teenager < AllFutures::Base
  validate :designated_driver?

  def :designated_driver?
    errors.add(:base, "is drunk") unless sober?

Our goal is to respond to this invalid state as part of the normal user experience, without actually raising an application level exception. To achieve this "successful failure", Rails gives us a family of methods that operate on the objects Array.

Many Rails developers think of errors as "the thing generated resources use to show validation failure messages". All Futures offers developers several compelling reasons to learn what ActiveModel::Errors has to offer someone building a reactive UI.

So, you think you have some errors

Let's start with a basic assumption: if your model is valid? - that is, true - then you shouldn't have any errors.

However, you might not want to run valid? because it runs all of your validations again, and that might be undesirable. The valid_email2 gem actually tests for valid MX servers, which could be slow. Or perhaps your validations connect with a paid API? (Don't actually do this!)

You can use the any? method to check for the presence of errors in the objects Array, and size to get a count. You can use attribute_names to access an Array of Symbols representing - you guessed it - attribute names that have errors associated with them.

There's also a details method that returns a Hash structure which conveys all error types for all attributes, eg. {:name=>[{:error=>:blank}]}

Manipulating errors

objects is an Array, so you should be able to just add and remove Error objects, right?

Actually, no - we don't want to manually instantiate ActiveModel::Error objects. Instead, we can make use of add , delete which make it easy to work with errors, even across multiple versions of Rails when the internal structure of Error objects might change over time.

You can clear your objects Array, but that won't make your model valid. To do that, you have to call valid? again - hopefully with the errors fixed.

full_messages vs messages

The error messages generated by ActiveModel::Errors are available with and without the attribute name prefixed, offering you a choice between "Name is invalid" (full_messages) and "is invalid" (messages).

Both are useful in different situations; you might not want an awkwardly-named attribute being converted into a proper noun, such as "State Province can't be empty".

The key to enlightenment is to spend time studying (and potentially modifying) the locale files for the languages that you support. Of course, you can also specify messages on a per-validation basis, but that can add complexity to your internationalization strategy.

Errors, for a specific attribute

The payoff for this chapter is that you can access the errors for a specific attribute, or for the :base model instance. When paired with All Future's ability to tell you if a given attribute is valid - even when the model itself might not be - this is a major level-up for reactive UI developers looking to give real-time feedback on an input field.

You can pass an attribute name as a Symbol to include? (aliased as both key? and has_key?) and receive a Boolean indicating whether that attribute has at least one error.

Pass a Symbol to messages_for(:attribute) or full_messages_for(:attribute) and get the errors for that attribute.

You can also access the ActiveModel::Error instances using the where method:

where(:name) # all name errors
where(:name, :too_short) # all name errors being too short
where(:name, :too_short, minimum: 2) # all name errors being too short and minimum is 2

To get the first full_message error for :name you would do something like this:

record.errors.where(:name).first.full_message # => "Name can't be blank"

Acting on specific errors

Most applications have two exception layers: the "successful failure" that comes from processing user input within predetermined constraints, and everything else. You need to determine which category of error that you're dealing with so you remain in control of the user experience, even when things go wrong.

There's actually two methods that test for the presence of specific errors. In the beginning, there was added?, while of_kind? was added in Rails 6.

added? is designed to use the same syntax as the add method. It matches against the specific options used to generate an error, or the final string generated by the error. It will not detect errors that do not match the exact signature of the query.

of_kind? is much more forgiving, in that it only accepts the attribute and error type but will return true if that error occurs, regardless of the options.

We find it confusing that there are two methods for this, too. Think of it like a John Hughes movie: added? is the uptight math teacher, while of_kind? is the coach who doles out tough love, but won't rat you out to the Vice Principal.

Last updated