Showing and Returning Errors

Validation messages arrive in the $errors bag for a template or as a 422 errors object for an API, and old() refills the form.

Syntax@error('field', 'bag') {{ $message }} @enderror

By the time a failed request reaches your code again the messages already exist. Displaying them is a matter of knowing where the framework put them.

The errors bag

ShareErrorsFromSession, part of the web middleware group, shares a ViewErrorBag with every view as $errors. It is always defined, and it is an empty bag when nothing failed, so no isset guard is needed. The methods are any(), all(), has('title'), first('title') and get('lines.*'); the last returns the messages for every matching index, keyed by lines.0 and so on.

@error('title') {{ $message }} @enderror is shorthand. It compiles to a bag lookup, an if on has(), and $message set to first(). The longhand is @if ($errors->has('title')) {{ $errors->first('title') }} @endif. A second argument selects a named bag: @error('email', 'login'), which pairs with $request->validateWithBag('login', ...) or the #[ErrorBag] attribute on a form request.

Refilling the form

The redirect flashed the input as well, so old('title') returns what was typed. old('title', $post->title) supplies a fallback, which lets one template serve both create and edit.

The API shape and the wording

A request that expects JSON gets a 422 with message and errors. Every value in errors is an array, even for one message, and message is the first error plus a summary such as "(and 2 more errors)". php artisan lang:publish copies validation.php into lang/en/: each rule's line uses the :attribute placeholder, the custom array overrides one field-and-rule pair, and the attributes array renames a field everywhere. A form request does the same for itself with messages() and attributes().

Example

Example · php
<?php
// app/Http/Requests/StoreSubscriberRequest.php

declare(strict_types=1);

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreSubscriberRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    /**
     * @return array<string, array<mixed>>
     */
    public function rules(): array
    {
        return [
            'email' => ['required', 'email', 'unique:subscribers,email'],
            'list_id' => ['required', 'integer', 'exists:mailing_lists,id'],
        ];
    }

    /**
     * Keyed "field.rule", using the rule's snake_case name. These beat
     * the lines in lang/en/validation.php for this request only.
     *
     * @return array<string, string>
     */
    public function messages(): array
    {
        return [
            'email.unique' => 'That address is already subscribed.',
            'list_id.exists' => 'Pick a list that still exists.',
        ];
    }

    /**
     * Replaces :attribute in every message for these fields, so the
     * default line reads "The mailing list field is required."
     *
     * @return array<string, string>
     */
    public function attributes(): array
    {
        return [
            'list_id' => 'mailing list',
        ];
    }
}

When to use it

  • Show a message under the input that caused it, not just at the top of the page.
  • Keep a long form filled in after a failed submit.
  • Give an API client a per-field errors object it can map onto its own form.
  • Rename a database column in user-facing messages, so list_id reads as "mailing list".

More examples

The form that displays them

The whole pattern: a summary at the top, a per-field message under each input, and old() on every value so a failed submit does not wipe the form. The old() fallback is what lets this one template handle both create and edit. If messages never appear, check that the route is in the web group, because without a session nothing is flashed.

Example · blade
{{-- resources/views/posts/create.blade.php --}}
<form method="POST" action="{{ route('posts.store') }}">
    @csrf

    {{-- $errors is always defined under the web group, empty when clean. --}}
    @if ($errors->any())
        <ul>
            @foreach ($errors->all() as $message)
                <li>{{ $message }}</li>
            @endforeach
        </ul>
    @endif

    <label for="title">Title</label>
    <input id="title" name="title" value="{{ old('title', $post->title ?? '') }}">

    {{-- Shorthand: $message is set to $errors->first('title'). --}}
    @error('title')
        <p>{{ $message }}</p>
    @enderror

    <label for="body">Body</label>
    <textarea id="body" name="body">{{ old('body', $post->body ?? '') }}</textarea>

    {{-- The same thing written out longhand. --}}
    @if ($errors->has('body'))
        <p>{{ $errors->first('body') }}</p>
    @endif

    <button type="submit">Save</button>
</form>

Reading errors for a repeated row

Nested validation keys have to be built in the template, because the bag stores lines.1.sku, not lines.*.sku. Looping with the index covers the common case; $errors->get() with a wildcard is for a summary where you do not want to re-render the rows.

Example · blade
{{-- Rules were 'lines' => ['array'], 'lines.*.sku' => ['required'] --}}
@foreach (old('lines', $order->lines) as $index => $line)
    <input name="lines[{{ $index }}][sku]" value="{{ $line['sku'] ?? '' }}">

    {{-- The error key holds the real index, not the wildcard. --}}
    @error('lines.'.$index.'.sku')
        <p>{{ $message }}</p>
    @enderror
@endforeach

{{-- Or pull every matching message at once. get() with a wildcard
     returns an array keyed by the concrete attribute:
     ['lines.1.sku' => ['The lines.1.sku field is required.']] --}}
@foreach ($errors->get('lines.*.sku') as $key => $messages)
    <p>{{ $key }}: {{ $messages[0] }}</p>
@endforeach

Renaming fields and messages for the whole application

The same two overrides a form request offers with messages() and attributes(), applied application-wide. Use this file when a field name appears on several endpoints, and the form request methods when the wording is specific to one. Copy the file with lang:publish first: there is no lang directory in a new Laravel 13 application, and the framework's own copy is inside vendor.

Example · php
<?php
// lang/en/validation.php  (php artisan lang:publish)

return [
    // The per-rule lines. :attribute is the field's display name.
    'required' => 'The :attribute field is required.',
    'unique' => 'The :attribute has already been taken.',

    // One field plus one rule. The rule key is its snake_case name.
    'custom' => [
        'email' => [
            'unique' => 'That address is already subscribed.',
        ],
    ],

    // The display name swapped into :attribute everywhere. Without an
    // entry here, list_id becomes "list id".
    'attributes' => [
        'list_id' => 'mailing list',
    ],
];

Discussion

  • Be the first to comment on this lesson.