Validations & Callbacks
Reviewed & published by Brayan K
Bad data is the enemy of every app. Rails validations let you declare the rules your records must satisfy, and callbacks let you hook into a record's lifecycle to normalise or react to changes.
Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll validate presence, uniqueness, length, and format, read errors, check valid?, and use callbacks like before_save and after_create.
What You'll Learn in This Lesson
- presence, uniqueness, length, and format validations
- Numericality and scoped uniqueness
- Checking validity with valid? and save
- Reading errors.full_messages
- Lifecycle callbacks (before_save, after_create)
- save versus save! behaviour
1️⃣ Declaring Validations
Put validates calls in the model. Common rules include presence, uniqueness, length, format, and numericality. If any fail, save returns false and the errors explain why.
# Validations live in the model and run before a record is saved.
# If any fail, save returns false and the record is NOT written.
class User < ApplicationRecord
validates :name, presence: true
validates :email, presence: true,
uniqueness: true,
format: { with: /\A[^@\s]+@[^@\s]+\z/ }
validates :age, numericality: { greater_than_or_equal_to: 0 }
validates :bio, length: { maximum: 140 }
end
# In the console:
#
# u = User.new(name: "", email: "nope")
# u.valid? # => false (runs the validations)
# u.save # => false (won't persist)
# u.errors.full_messages
# # => ["Name can't be blank", "Email is invalid"]
#
# good = User.new(name: "Ada", email: "ada@x.io", age: 30)
# good.save # => true2️⃣ Lifecycle Callbacks
Callbacks fire at points in a record's life. before_validation and before_save run on every save; after_create runs only once, after a new record is inserted. Use them to normalise or derive data.
# Callbacks hook into the lifecycle of a record (validate, save,
# create, update, destroy). Use them to normalise or derive data.
class Article < ApplicationRecord
validates :title, presence: true
before_validation :strip_title
before_save :set_slug
after_create :notify_followers
private
def strip_title
self.title = title.strip if title.present?
end
def set_slug
# runs before every save (create AND update)
self.slug = title.downcase.gsub(/\s+/, "-")
end
def notify_followers
# runs once, only after a NEW record is inserted
# (e.g. enqueue an email job here)
end
end
# Typical lifecycle order for a new record on save:
# before_validation -> validate -> before_save
# -> before_create -> INSERT -> after_create -> after_saveYour turn. Finish the validations on a Product model so name, sku, and price are all checked.
# Rails is not in your browser and ActiveRecord needs a database, so the
# models above cannot run here. The RULES can: a validation is only a
# predicate over an object. These are the same four the User model
# declares, written out so you can watch them accept and reject.
EMAIL = /\A[^@\s]+@[^@\s]+\z/
def errors_for(name:, email:, age: 0, bio: "", taken_emails: [])
errors = []
errors << "Name can't be blank" if name.to_s.strip.empty?
errors << "Email can't be blank" if email.to_s.strip.empty?
errors << "Email is invalid" unless email.to_s.empty? || email.match?(EMAIL)
errors << "Email has already been taken" if taken_emails.include?(email)
errors << "Age must be greater than or equal to 0" if age < 0
errors << "Bio is too long (maximum is 140 characters)" if bio.length > 140
errors
end
# The exact case the lesson's console transcript above describes:
bad = errors_for(name: "", email: "nope")
puts "u.valid? => #{bad.empty?}"
puts "u.errors.full_messages"
puts " => #{bad.inspect}"
# And the three cases it does not:
good = errors_for(name: "Ada", email: "ada@x.io", age: 30)
puts "good.valid? => #{good.empty?}"
dupe = errors_for(name: "Bo", email: "ada@x.io", taken_emails: ["ada@x.io"])
puts "duplicate email => #{dupe.inspect}"
long = errors_for(name: "Cy", email: "cy@x.io", bio: "x" * 141)
puts "141-character bio => #{long.inspect}"
# Note that uniqueness is the odd one out: every other rule can be decided
# from the record alone, but "has this email been used?" has to ask the
# database. That is why Rails still needs a unique INDEX behind it — two
# requests can both pass the check a moment apart and both then insert.Mini-Challenge: A Signup Model
Validate a Signup and add a before_save callback that downcases the email before it is stored.
# 🎯 MINI-CHALLENGE: a Signup model with a normalising callback.
#
# 1. validates :username, presence: true, length: { minimum: 3 }
# 2. validates :email, presence: true, uniqueness: true
# 3. Add a before_save callback that downcases the email
# (so "ADA@X.IO" is stored as "ada@x.io").
#
# Then check that a record with a blank username is NOT valid,
# and that a valid record stores a lowercased email.
#
# ✅ Goal:
# s = Signup.new(username: "ada", email: "ADA@X.IO")
# s.save
# s.email # => "ada@x.io"
# your code hereCommon Errors (and the fix)
- Silent failed save — save returns false on invalid data; check the return value or use save!.
- Relying on uniqueness alone — Add a unique database index too; the validation can race under load.
- Heavy work in callbacks — Sending emails or calling APIs in callbacks makes models hard to test; prefer jobs or services.
- Returning false in a callback — In modern Rails use throw :abort to halt the chain, not a bare false.
- Forgetting to read errors — After a failed save, inspect errors.full_messages to see what went wrong.
Pro Tips
- 💡 Validate at the right layer: model validations for friendly messages, database constraints for true guarantees.
- 💡 Use allow_blank/allow_nil: to skip a rule when a value is intentionally empty.
- 💡 Keep callbacks tiny: a one-line data tweak is fine; complex side effects belong elsewhere.
📋 Quick Reference — Validations & Callbacks
| Goal | Code |
|---|---|
| Required field | validates :name, presence: true |
| No duplicates | validates :email, uniqueness: true |
| Length limit | length: { maximum: 140 } |
| Pattern | format: { with: /.../ } |
| Check validity | record.valid? |
| Before each save | before_save :method |
🎉 Lesson Complete!
- ✅ validates declares rules like presence, uniqueness, length, format
- ✅ valid? runs them; save returns false on failure
- ✅ errors.full_messages explains what went wrong
- ✅ Callbacks like before_save and after_create hook the lifecycle
- ✅ Pair model validations with database constraints for real guarantees
Practice quiz
Which validation ensures a column is not blank?
- validates :name, length: true
- validates :name, presence: true
- validates :name, required: true
- validates :name, not_null: true
Answer: validates :name, presence: true. presence: true rejects nil and blank values.
What does validates :email, uniqueness: true enforce?
- The email is formatted correctly
- No two records share the same email value
- The email is present
- The email is lowercase
Answer: No two records share the same email value. uniqueness: true rejects a record whose value already exists for that attribute.
Which method returns true only when all validations pass?
- validate!
- errors?
- check
- valid?
Answer: valid?. valid? runs validations and returns true if there are no errors.
After a failed save, where are the human-readable messages?
- record.warnings
- record.errors.full_messages
- record.messages
- record.invalid
Answer: record.errors.full_messages. errors.full_messages holds the readable list after validation fails.
Which validation limits a string to a maximum of 140 characters?
- validates :body, max: 140
- validates :body, size: 140
- validates :body, limit: 140
- validates :body, length: { maximum: 140 }
Answer: validates :body, length: { maximum: 140 }. length: { maximum: 140 } caps the length; minimum and is are also available.
Which callback runs just before a record is written on both create and update?
- after_create
- before_destroy
- before_save
- after_find
Answer: before_save. before_save runs prior to writing, for both new and existing records.
Which callback runs only after a brand-new record is first inserted?
- after_create
- before_save
- after_save
- before_validation
Answer: after_create. after_create runs once, after the initial INSERT of a new record.
What does save (without a bang) return when validation fails?
- It raises an exception
- It returns false
- It returns nil and retries
- It returns the errors array
Answer: It returns false. save returns false on validation failure; save! raises instead.
Which validation checks a value matches a regular expression?
- validates :code, pattern: /.../
- validates :code, regex: /.../
- validates :code, format: { with: /.../ }
- validates :code, match: /.../
Answer: validates :code, format: { with: /.../ }. format: { with: /regex/ } validates against a pattern.
How can you scope uniqueness so an email is unique per account?
- uniqueness: { scope: :account_id }
- uniqueness: :account_id
- unique_within: :account
- uniqueness: { per: :account }
Answer: uniqueness: { scope: :account_id }. uniqueness: { scope: :account_id } makes the value unique within each account.
Continue this course
- Previous: Associations
- Next: Forms & Strong Parameters — form_with, strong parameters, CSRF protection and flash
- Quick reference: Ruby cheat sheet
Frequently asked questions
What is the difference between save and save!?
Both run validations first. save returns true if the record was written and false if validation failed, so you typically branch on it: if @user.save then redirect, else re-render the form. save! is the bang version: instead of returning false on failure, it raises an ActiveRecord::RecordInvalid exception. Use plain save in controllers where you want to show errors to the user, and save! in seeds, scripts, or background jobs where a failure genuinely should blow up. The same pairing exists for create/create! and update/update!.
How do I show validation errors to the user?
When a save fails, the messages are on the record's errors object. record.errors.full_messages gives an array of readable strings like "Email can't be blank", and record.errors[:email] gives just the messages for one attribute. In a controller you usually re-render the form with the invalid object, and the form view loops over those messages to display them next to the fields. The messages are generated from your validation rules and Rails' built-in locale text, which you can customise.
When should I use a callback versus putting the logic elsewhere?
Callbacks are great for small, model-intrinsic data tweaks: normalising a string with before_validation, deriving a slug with before_save, or setting a default. They run automatically on every save, which is their strength and their danger. For anything with side effects beyond the record itself — sending email, calling external services, complex multi-model workflows — many Rails developers prefer to move that logic out of callbacks into explicit service objects or background jobs, because hidden callbacks make code hard to follow and test. Keep callbacks simple and predictable.
Does uniqueness: true guarantee no duplicates?
Not on its own. The uniqueness validation runs a SELECT to check for an existing row, but two requests can pass that check at the same instant and both insert — a race condition. To truly guarantee uniqueness you also add a unique index in the database via a migration (add_index :users, :email, unique: true). The validation gives friendly error messages for normal cases; the database index is the real safety net that rejects duplicates even under concurrency. Use both together.