In a Laravel Blade view, how do you show validation errors after a failed form submission, and where does the $errors variable come from?
answer
- errors flashed to the session on redirect
- ShareErrorsFromSession shares the bag
- ViewErrorBag wraps named MessageBags
- @error sets $message to the first
- first() returns '' when absent
basics
~20 sA failed form validation redirects back and flashes the errors to the session; ShareErrorsFromSession in the web group shares them with every view as $errors, a ViewErrorBag. Read them with @error('field') and $message, or first(), has() and all().
solid answer
~40 sWhen validation fails on an ordinary form post, Laravel throws a `ValidationException`; the exception handler redirects to the previous URL and flashes a `ViewErrorBag` under the session key `errors`. On the next request `ShareErrorsFromSession`, part of the `web` middleware group, shares that bag with every view as `$errors`, or shares an empty `ViewErrorBag` when there is none, so the variable is always defined there. In Blade, `@error('email') ... {{ $message }} ... @enderror` renders only when the field has a message, and `$message` holds the first one. On the bag itself, `has('email')` returns a bool, `first('email')` returns one string (an empty string when there is none), `get('email')` returns that field's array, and `all()` returns a flat list of every message, which suits a summary block guarded by `$errors->any()`.
code
html · 21 lines<form method="POST" action="/benefits/apply">
@csrf
@if ($errors->any())
<div class="alert">
<p>Please fix {{ $errors->count() }} problem(s):</p>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<label for="email">Email</label>
<input id="email" name="email" type="email"
class="@error('email') is-invalid @enderror">
@error('email')
<p class="error">{{ $message }}</p>
@enderror
</form>go deeper
Recall the round trip: redirect back, errors flashed, $errors in the view. Be able to write an @error block with $message and a summary list from $errors->all().
Explain that ShareErrorsFromSession shares an empty ViewErrorBag when nothing was flashed, that unqualified calls go to the default bag, and the return types of first(), has(), get() and all().
Show you know where the model breaks: views rendered outside the web group, queued mail, dot-notation keys for array inputs, and why messages must be echoed escaped.
Frame the choice between inline field messages and a summary block as an accessibility and consistency decision the team should settle once in shared Blade components.
## The round trip from a failed rule to the page Classic server-rendered forms in Laravel follow the **Post/Redirect/Get** pattern, and validation errors ride along that redirect. The sequence for a `POST` from a benefits application form is: 1. The controller (or a form request) validates the input and one or more rules fail. 2. The validator throws `Illuminate\Validation\ValidationException`. 3. The framework's exception handler sees that the request does not expect JSON and builds a **redirect to the previous URL**. It flashes the validator's messages into the session under the key `errors`, wrapped in a `ViewErrorBag` (the old input is flashed alongside it, a separate topic). 4. The browser follows the redirect with a `GET`. On that request the `web` middleware group runs `Illuminate\View\Middleware\ShareErrorsFromSession`. 5. That middleware calls `View::share('errors', ...)` with the flashed bag, or with a brand-new empty `ViewErrorBag` if the session holds none. 6. Every Blade view rendered during that request can now read `$errors`. Because the data was **flashed**, it is gone on the request after that. The important consequence of step 5 is that `$errors` is **always defined** in views rendered through the `web` group, even on the first visit. You never need `isset($errors)` there; `$errors->any()` is simply `false`. ## What `$errors` actually is `$errors` is an `Illuminate\Support\ViewErrorBag`, a small container that maps **bag names** to `MessageBag` instances. Validation normally writes to the bag named `default`. Any method you call directly on the `ViewErrorBag` that it does not define itself (`first`, `has`, `get`, `all` and so on) is forwarded by `__call` to the `default` bag, which is why `$errors->first('email')` works without naming a bag. Its own `any()` and `count()` also look only at `default`. The `MessageBag` (`Illuminate\Support\MessageBag`) stores messages as an array keyed by field name, each key holding a list of strings, because one field can fail several rules at once. ## Reading messages: the `MessageBag` methods | Call | Returns | Typical use | |---|---|---| | `has('email')` | `bool`, true when the field has a message | toggling an `is-invalid` class | | `has('email', 'phone')` | `bool`, true only if **every** key has one | rare; all-of check | | `hasAny(['email', 'phone'])` | `bool`, true if **any** key has one | grouped fieldsets | | `first('email')` | `string`, the first message, or `''` | the message under an input | | `first()` | `string`, the first message of any field | a one-line banner | | `get('email')` | `array` of that field's messages | listing every failure for one field | | `all()` | flat `array` of every message | a summary list at the top of the form | | `any()` / `count()` | `bool` / `int` | guarding the summary block | Two details trip people up. First, `first()` returns an **empty string**, never `null`, when nothing matches, so a strict `null` comparison never fires. Second, `all()` **drops the field names**: it is ideal for a summary, useless for placing a message next to its input. ## The `@error` directive `@error('email')` compiles to PHP that fetches the bag (`default` unless you pass a second argument naming another bag), checks `has('email')`, and assigns `$message = first('email')`. The block body renders only when that check passes, and `@enderror` unsets `$message` again, restoring any outer `$message` variable the view already had. An `@else` branch is allowed for a valid state. Because it is a plain conditional, it can also sit inside an attribute: `class="@error('email') is-invalid @enderror"`. ## Common traps - **Views outside the `web` group.** `$errors` is not a Blade global; it exists only because `ShareErrorsFromSession` shared it. A view rendered for an API route, or a mail template rendered later by a queue worker, never receives it, and `@error` fails there with an undefined variable. - **Echo with `{{ }}`.** Messages can contain submitted values (the `:input` placeholder), so print them escaped. - **Array fields use dot keys.** An input named `dependents[0][name]` reports under `dependents.0.name`; that is the key `@error` needs. - **Errors survive one request only.** A second refresh after the redirect shows a clean form, which is correct behaviour, not a lost session. - **Do not re-validate in the view.** The bag is the single source of truth for what failed; the view only reads it.
- What does `$errors->first('phone')` return when the phone field passed validation?An empty string, not `null`. `MessageBag::first()` falls back to `''` when no message exists for the key, so `{{ $errors->first('phone') }}` prints nothing and a strict `null` check never matches. Use `$errors->has('phone')` for a boolean test; it is true only when `first()` would return a non-empty string.
- Why can a queued mail template that uses `@error` fail with an undefined `$errors` variable?`$errors` is not a Blade global. `ShareErrorsFromSession`, part of the `web` middleware group, shares it with views during a web request. A queued mailable is rendered later by a worker process that never ran that middleware, so the variable was never shared and the compiled `@error` code, which calls `$errors->getBag()`, has nothing to call.
- How does `has()` behave when you pass it several field names?`$errors->has('email', 'phone')` (or an array) is an all-of check: it returns true only when every listed key has at least one message. For an any-of check use `hasAny(['email', 'phone'])`, and `missing()` is the negation of `hasAny()`.
A failed submission is like a clerk returning a paper form with sticky notes on the wrong boxes: the notes (the flashed bag) travel back with the form exactly once, and @error is you checking whether a given box has a note stuck on it.
saying these in an interview costs you the question
- $errors is undefined on a first visit, so every use needs an isset() guard.
- first('field') returns null when the field has no error.
- $errors is a Blade global available in every view, including queued mail templates.
- Validation errors stay in the session until the form is submitted again successfully.
- $errors->all() returns the messages grouped under their field names.