Humane database errors in ActiveRecord

ActiveRecord doesn't really play well with database constraints and other builtin data consistency mechanisms.

It comes with its own mechanisms for ensuring consistency, but it can be coerced into using the mechanisms from the database as well.

Ensuring unique names in a users table

Imagine you've implemented an ActiveRecord model named User, with the following schema:

create_table "users", force: true do |t|
  t.string "name"
end

Names need to be unique across users, so you've also added a unique constrain on the name column:

add_index "users", ["name"], unique: true

This way we know we'll never get two rows with the same name value in the database:

>> User.create!(:name => "Bob")
=> #<User id: 1, name: "Bob">
>> User.create!(:name => "Bob")
ActiveRecord::RecordNotUnique: PG::UniqueViolation: ERROR:  duplicate key value violates unique constraint "index_users_on_name"
DETAIL:  Key (name)=(Bob) already exists.
: INSERT INTO "users" ("name") VALUES ($1) RETURNING "id"

Not the cleanest of results but pretty much as expected: We hit the unique constraint, the database refuses to store the data, and ActiveRecord raises an exception.

If this were to happen in a running Rails app, the end user would get a HTTP 500 error page, which is not the most user friendly of things.

Screenshot of server error in development mode

Validations

One way to work around this is to add a uniqueness validation of the name attribute: validates :name, :uniqueness => true. This is fine, gives the users readable (and localizable) error messages and work in by far the majority of cases.

If you can, by all means use the validation.

Rescuing database errors

But how do we handle those cases where the validation won't suffice and we don't want to show HTTP 500 error pages to our users?

If we create our save method, we can actually catch the database error before it bubbles up to the user interface and - hopefully - do something better with it:

class User < ActiveRecord::Base
  def save(*args)
    super
  rescue ActiveRecord::RecordNotUnique => error
    errors[:base] << error.message
    false
  end
end

Now, at least the user gets a clue as to what has happened, but perhaps not in the most user friendly fashion:

Screenshot of database error rendered as a validation error

You could naturally place any other message into the errors object if you so desire - for example one that makes more sense to normal people.