Matthew Lindfield Seager

Matthew Lindfield Seager

User Friendly Error Messages for Multiple Fields in Rails

The built in error system in Rails, specifically displaying errors in an HTML form when data isn’t valid, is mature and works really well for simple cases.

Adding validates :name, presence: true to a user model is all we need to do to prevent people from saving a user without a name. Straight out of the box we automatically get a count of the errors and details of each error: Error messages "out of the box"

Rails also wraps the field and label in a div with class field_with_errors so two lines of CSS in the application layout is all it takes to initially add some basic error highlighting to an “out of the box” application:

<style>
  .field_with_errors { color: #b22; }
  .field_with_errors input { background-color: #fcc; }
</style>

Forms errors with two lines of CSS added

More complicated checks are also possible but displaying the results of those checks in a user-friendly way wasn’t very intuitive to me. This post documents the things I tried and the solution I ended up with.

The end goal is to require at least one of two fields to be present for the form to be valid. To make that clear to the user, I want it to show up as a single error but with both fields highlighted:

Single error but with two fields highlighted


Solution 1

The approach you’ll find suggested multiple times on StackOverflow is to add a check to both fields:

validates :email, presence: {unless: :username?}
validates :username, presence: {unless: :email?}

Naive solution, checking both fields separately

Technically this works but I don’t like the way it shows two different errors or that the error messages aren’t entirely truthful. Even if we customise the error messages to make them more accurate, it doesn’t really improve the situation as the error messages are still redundant (not to mention awkwardly worded as they must start with the name of the field):

validates :email, presence: {unless: :username?, message: "or Username must be present"}
validates :username, presence: {unless: :email?, message: "or Email must be present"}

Updating the messages doesn't really help the situation


Solution 2

The next solution is to try a custom validation. By adding an error to :base it applies to the whole model object, rather than an individual field:

validate :email_or_username

private
def email_or_username
  if email.blank? && username.blank?
    errors.add(:base, message: "At least one of Email and Username must be provided")
  end
end

This gets us closer to our desired end state of one error for one problem but we lose field highlighting:

One error for one problem but no field highlighting

We can modify the validation method to get field highlighting but then we end up with three errors:

if email.blank? && username.blank?
  errors.add(:base, message: "At least one of Email and Username must be provided")
  errors.add(:email)
  errors.add(:username)
end

Correct field highlighting but too many errors

Solution 3

We need some way to distinguish the field errors from the overall record error in a way that will make sense to us in the future. As of Rails 6.1, errors are first class objects and the optional second argument to ActiveModel::Errors.add is a type symbol.

If we add an explicit type to the errors that will help us identify them later:

# app/models/user.rb
if email.blank? && username.blank?
  errors.add(:base, :email_and_username_blank, message: "At least one of Email and Username must be provided")
  errors.add(:email, : highlight_field_but_hide_message)
  errors.add(:username, : highlight_field_but_hide_message)
end

Then in our generated view we need to somehow ignore or delete the ...hide_message errors. At first I thought I could use ActiveModel::Errors.delete to delete them but there are multiple problems with that approach:

  • to match on a type you first have to match on attribute (either looping through all attribute names or listing attributes explicitly)
  • deleting the error prevents the field from being highlighted so you need to render the fields first, then delete the errors, then show the error message (which would also require some CSS shenanigans to move it back to the top as per the original requirement)

The simplest approach seems to be to take advantage of the fact that Errors is Enumerable and simply reject the errors we don’t want to include in the summary:

# app/views/users/_form.html.erb
<%= form_with(model: user) do |form| %>
  <% visible_errors = user.errors.reject{ |e| e.type == :highlight_field_but_hide_message } %>
  <% if visible_errors.any? %>
    <div class="text-red-700 border border-red-700 border-rounded m-2 p-2 bg-red-200 max-w-md">
      <h2 class="text-xl"><%= pluralize(visible_errors.count, "error") %> prohibited this user from being saved:</h2>

      <ul class="list-disc list-inside">
        <% visible_errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>
<% end %>

One error message but multiple fields highlighted


Conclusion

With a small amount of additional effort it’s possible to show a single error message but highlight multiple fields. My next task is to override Rails.application.config.action_view.field_error_proc so I can use Tailwind CSS to style the fields with errors.