Skip to content

Lesson 4 — Error handling strategies

Validation errors can be displayed in three ways. The right choice depends on the app, the form size, the target audience, and any design standard you’re working to. Phlex makes all three equally straightforward.

The error prop

Every input primitive accepts a single error prop that controls both styling and message display:

1
prop :error, _Nilable(String), default: -> { nil }

Three states:

  • nil — no error, normal styling
  • "" — error styling (red border, aria-invalid="true") but no message
  • "can't be blank" — error styling plus the message displayed below the input

This gives you full control over the error experience from the call site. No separate error display method needed — the primitive owns the complete inline error experience.

Strategy 1 — Inline per field

Pass the error message directly to each input. The primitive handles the red border, aria-invalid, and the message in one call:

1
2
3
4
5
6
7
8
9
form_with(model: @board) do |f|
  TextInput(
    field: :name,
    label: "Board name",
    value: @board.name,
    error: @board.errors[:name].first
  )
  Button(label: "Save", type: "submit")
end

@board.errors[:name].first returns nil when there are no errors — so the input renders normally on first load and shows the error only after a failed submission.

Strategy 2 — Summary at the top

Use ErrorSummary to list all errors before the form fields. Pass "" to each input to highlight the offending fields without repeating the message:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
form_with(model: @board) do |f|
  ErrorSummary(errors: @board.errors)

  TextInput(
    field: :name,
    label: "Board name",
    value: @board.name,
    error: @board.errors[:name].any? ? "" : nil
  )
  Button(label: "Save", type: "submit")
end

The empty string "" gives the red border so users can see which field needs attention, without duplicating the message that’s already in the summary.

Strategy 3 — Both

Summary at the top for orientation, inline messages for precision:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
form_with(model: @board) do |f|
  ErrorSummary(errors: @board.errors)

  TextInput(
    field: :name,
    label: "Board name",
    value: @board.name,
    error: @board.errors[:name].first
  )
  Button(label: "Save", type: "submit")
end

ErrorSummary — an optional component

If render_error_summary appears across multiple views, extract it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# app/components/error_summary.rb
class Components::ErrorSummary < Components::Base
  prop :errors, _Any

  def view_template
    return unless @errors.any?

    div(
      class: "rounded-md bg-red-50 border border-red-200 p-4",
      role:  "alert"
    ) do
      p(class: "text-sm font-medium text-red-800 mb-2") do
        plain "Please fix the following errors:"
      end
      ul(class: "list-disc list-inside text-sm text-red-700 space-y-1") do
        @errors.full_messages.each { |msg| li { msg } }
      end
    end
  end
end

Usage: ErrorSummary(errors: @board.errors)

Which strategy for KanbanFlow?

BoardForm has one field — inline errors are the right choice. Longer forms (the invite form in Module 11, card detail in Module 12) may warrant a summary. We choose per form as we build them.