rails-error-prevention

Comprehensive checklist and prevention strategies for common Ruby on Rails errors. Use this skill BEFORE writing any Rails code to prevent ViewComponent template errors, ActiveRecord query mistakes, m

SKILL.md

GitHub

Rails Error Prevention

Critical Rule: Review the relevant section BEFORE writing code, not after errors occur.

The #1 Cause of Runtime Errors

WRONG: Service has method → View can call it through component
RIGHT: Service has method + Component EXPOSES it = View can call it

Every layer must explicitly expose what the next layer needs. Views cannot reach through components to access service internals.

Error Prevention Decision Tree

What are you building?
│
├─ ViewComponent
│   └─ Go to: ViewComponent Errors
│
├─ ActiveRecord query with GROUP BY
│   └─ Go to: GROUP BY Errors
│
├─ View that calls component methods
│   └─ Go to: Method Exposure Verification
│
├─ Controller action
│   └─ Go to: Controller Errors
│
└─ Code with potential nil values
    └─ Go to: Nil Errors

ViewComponent Errors

Template Not Found

Error: Couldn't find a template file or inline render method for {Component}

Verification before creating:

# Check existing pattern (inline vs file template)
head -50 $(find app/components -name '*_component.rb' | head -1)
grep -l 'def call' app/components/**/*_component.rb | head -3

Requirements:

app/components/namespace/name_component.rb      ← Class file
app/components/namespace/name_component.html.erb ← Template (unless inline)

NEVER create a component without checking the project's template pattern first.

Helper Method Errors

Error: undefined local variable or method 'link_to' for #<MyComponent>
Hint: Did you mean `helpers.link_to`?

Rule: ALL Rails helpers need helpers. prefix in ViewComponents:

# WRONG                    # RIGHT
link_to(...)              helpers.link_to(...)
image_tag(...)            helpers.image_tag(...)
number_to_currency(...)   helpers.number_to_currency(...)
time_ago_in_words(...)    helpers.time_ago_in_words(...)
content_tag(...)          helpers.content_tag(...)

Or delegate explicitly:

class MyComponent < ViewComponent::Base
  delegate :link_to, :number_to_currency, to: :helpers
end

Method Exposure Verification

MANDATORY before writing view code:

# 1. List all methods view will call
grep -oE '@[a-z_]+\.[a-z_]+' app/views/{path}/*.erb | sort -u

# 2. List all public methods in component
grep -E '^\s+def [a-z_]+' app/components/{component}_component.rb

# 3. Any view call without matching component method = BUG

Fix missing methods:

# Option 1: Delegation (simple pass-through)
delegate :calculate_total, :success_rate, to: :@service

# Option 2: Wrapper (for transformation)
def formatted_total
  helpers.number_to_currency(@service.calculate_total)
end

# Option 3: Expose service (use sparingly)
attr_reader :service
# View calls: component.service.calculate_total

GROUP BY Errors (PostgreSQL)

Error: PG::GroupingError: column "table.column" must appear in GROUP BY clause

Rule: Every non-aggregated SELECT column MUST be in GROUP BY.

NEVER Combine These:

# FATAL: includes/preload + group
Task.includes(:user).group(:status).count  # ERROR!

# FATAL: select with non-grouped columns
Task.select(:status, :id).group(:status)   # ERROR! (id not grouped)

Correct Patterns:

# Simple aggregation (no associations needed)
Task.group(:status).count
# => { "pending" => 10, "completed" => 25 }

# Multiple columns
Task.group(:status, :task_type).count
# => { ["pending", "express"] => 5, ... }

# If you need associated data, query separately
status_counts = Task.group(:status).count
tasks_by_status = status_counts.keys.each_with_object({}) do |status, hash|
  hash[status] = Task.where(status: status).includes(:user).limit(5)
end

Pre-query checklist:

[ ] Is this a GROUP BY query?
[ ] Are ALL SELECT columns either in GROUP BY or aggregates?
[ ] Have I removed includes/preload/eager_load?
[ ] Tested in rails console first?

N+1 Queries

Detection: Multiple identical queries in logs.

# WRONG - N+1
tasks.each { |t| puts t.user.name }  # Query per task!

# RIGHT
tasks.includes(:user).each { |t| puts t.user.name }  # 2 queries total

Loading strategy:

includes(:user)      # Smart (preload or eager_load)
preload(:user)       # Separate queries (can't filter)
eager_load(:user)    # Single LEFT JOIN (can filter)
joins(:user)         # INNER JOIN only (no loading)

Nil Errors

Error: undefined method 'foo' for nil:NilClass

Prevention patterns:

# Safe navigation
user&.profile&.settings&.theme

# Guard clause
return unless user&.profile

# With default
user&.profile&.theme || 'default'

# Early return
def process(user)
  return if user.nil?
  # ...
end

Controller Errors

Missing Template

Error: ActionView::MissingTemplate

Every non-redirect action needs:

  • A view file at app/views/{controller}/{action}.html.erb, OR
  • Explicit render: render json:, render partial:, head :ok

Unknown Action

Error: AbstractController::ActionNotFound

Causes:

  • Route points to non-existent action
  • Action is private or protected (must be public)

Ambiguous Column (Joins)

Error: PG::AmbiguousColumn: column reference "status" is ambiguous

Always qualify columns when joining:

# WRONG
User.joins(:tasks).where(status: 'active')

# RIGHT
User.joins(:tasks).where(users: { status: 'active' })
# OR
User.joins(:tasks).where('users.status = ?', 'active')

Quick Prevention Checklists

Before Creating ViewComponent

[ ] Checked existing template pattern (inline vs file)
[ ] Both .rb and .html.erb created (unless inline)
[ ] ALL methods view needs are PUBLIC in component
[ ] ALL Rails helpers use `helpers.` prefix

Before Writing View Code

[ ] Listed ALL methods view will call on component
[ ] EACH method exists in component class (verified)
[ ] Missing methods added to component FIRST

Before GROUP BY Query

[ ] ALL SELECT columns are grouped or aggregated
[ ] NO includes/preload/eager_load used
[ ] Tested in rails console

Before Any Code

[ ] Inspected existing similar code for patterns
[ ] All dependencies exist (methods, files, routes)
[ ] Method exposure verified across all layers