skip to content

On a Laravel page with separate login and registration forms that both have an email field, how do you keep their validation errors apart?

level: middleimportance: should knowfreq 36%

answer

  1. one ViewErrorBag, many MessageBags
  2. bag name chosen when errors are flashed
  3. $errorBag or #[ErrorBag] on form requests
  4. @error('email', 'login')
  5. any() counts only the default bag

basics

~10 s

Flash one form's errors into a named bag, via validateWithBag('login', ...), a form request's $errorBag or #[ErrorBag('login')], or withErrors($validator, 'login'), then read it with $errors->login->first('email') or @error('email', 'login').

solid answer

~40 s

`$errors` is a `ViewErrorBag`, a map from bag name to `MessageBag`; every unqualified call (`first`, `has`, `any`, `count`) goes to the bag named `default`, so two forms that both validate `email` would otherwise share one message. Give one form its own bag when the errors are flashed: `$request->validateWithBag('login', $rules)`, a form request with `protected $errorBag = 'login'` or, new in Laravel 13, the `#[ErrorBag('login')]` class attribute, or `withErrors($validator, 'login')` on a manual redirect. In Blade, read it with `$errors->login->first('email')`, since the magic property returns an empty `MessageBag` when the bag was never flashed, or with `@error('email', 'login')`. The catch is that `$errors->any()` ignores named bags, so a banner for the login form must test `$errors->login->any()` or `$errors->hasBag('login')`.

code

php · 18 lines
php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\Attributes\ErrorBag;
use Illuminate\Foundation\Http\FormRequest;

#[ErrorBag('login')]
class ClaimantLoginRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'email' => ['required', 'email'],
            'password' => ['required'],
        ];
    }
}

go deeper

for a junior

Recall that $errors can hold several named bags and that @error takes the bag name as a second argument.

for a middle

Explain how the bag name is chosen at flash time (validateWithBag, $errorBag or #[ErrorBag], _error_bag, withErrors) and that unqualified calls hit only the default bag.

for a senior

Spot the silent failures: an any()-guarded banner that never shows, a misspelled bag that reads as empty, and the assumption that bags affect JSON responses.

for a principal

Weigh named bags against splitting the forms onto separate pages or components, and set one naming convention so views and form requests cannot drift apart.

## Why one bag is not enough A claimant portal for a benefits service often shows **sign in** and **start a new claim** on the same page. Both forms have an `email` input, and both post to different routes that redirect back to that page on failure. With a single bag, a failed sign-in writes `email => ["The email field must be a valid email address."]` into the default bag, and **both** email inputs light up, because both `@error('email')` blocks read the same key. Named error bags solve this by storing each form's messages under a separate name. ## The structure: `ViewErrorBag` holds `MessageBag`s `Illuminate\Support\ViewErrorBag` keeps an array of bags keyed by name. Its API is small: | Member | Behaviour | |---|---| | `getBag('login')` | the `login` bag, or a **new empty `MessageBag`** if it does not exist | | `$errors->login` (magic `__get`) | same as `getBag('login')` | | `hasBag('login')` | `true` when that bag was flashed | | `getBags()` | every bag, keyed by name | | `any()`, `count()` | only the `default` bag | | any other method, via `__call` | forwarded to the `default` bag | Because `getBag()` never returns `null`, `$errors->login->first('email')` is safe on a fresh page load: it returns an empty string. ## Putting errors into a named bag The bag name is chosen at the moment the errors are flashed. When a `ValidationException` is turned into a redirect, the handler calls `withErrors($exception->errors(), $bagName)`, and the bag name comes from: 1. **The exception's `errorBag` property**, `default` unless something set it. `$request->validateWithBag('login', $rules)` sets it to `login` when validation fails. 2. **A form request's bag**: `protected $errorBag = 'login';` or, from Laravel 13.0, the class attribute `#[ErrorBag('login')]` (`Illuminate\Foundation\Http\Attributes\ErrorBag`). The form request copies it onto the exception it throws. 3. **A hidden `_error_bag` input**: the handler reads `$request->input('_error_bag', $exception->errorBag)`, so a form can pick its bag with `<input type="hidden" name="_error_bag" value="login">` and no controller change. 4. **A manual redirect**: `redirect()->back()->withErrors($validator, 'login')` when you build the response yourself. ## Reading a named bag in Blade - `@error('email', 'login') {{ $message }} @enderror` passes the bag name as the directive's second argument. - `$errors->login->first('email')`, `->has('email')`, `->all()` work exactly like the default bag. - `$errors->hasBag('login')` answers "did the sign-in form fail on the last request?" without caring which field. ## Traps - **`$errors->any()` stays false** when only the `login` bag has messages. A shared "please fix the errors below" banner guarded by `any()` silently disappears for the sign-in form. - **`@error('login.email')` is not a named-bag lookup**; it searches the default bag for a key literally called `login.email` (the dot syntax used for array inputs). - **Named bags are a redirect feature.** The 422 JSON body for an API request carries `message` and `errors` and ignores the bag name entirely. - **Pick names once.** A form request that uses `#[ErrorBag('login')]` and a Blade view that reads `$errors->signin` fail quietly: the view simply sees an empty bag, so there is no exception to warn you. - **Version check.** On Laravel 12 and earlier, form requests only have the `$errorBag` property; the attribute is new in 13. ## Choosing named bags or another layout Named bags are the smallest change when two server-rendered forms must share one page and one URL, but they are not the only option: - **Separate pages**: if each form can live on its own route, the default bag is enough and there is nothing to name. - **Different field names**: renaming one input to `login_email` avoids the clash, but it leaks a layout concern into the validation rules and the request data. - **Script-driven forms**: a form that submits with JavaScript and reads the 422 JSON body keeps its errors in its own component state, so bags never come into play. When named bags are the right fit, keep the name in one place, such as the form request's attribute plus a constant the Blade view references, so a rename cannot leave the view quietly reading an empty bag.

  • What does `$errors->login->first('email')` return on a page load where no login bag was flashed?
    An empty string. The magic property calls `getBag('login')`, which returns a new empty `MessageBag` when the bag is missing, and `first()` on an empty bag returns `''`. Nothing throws, which is convenient but also means a misspelled bag name fails silently.
  • How can a Blade form choose its error bag without touching the controller or form request?
    Add a hidden `_error_bag` input. When the exception handler converts a `ValidationException` into a redirect, it uses `$request->input('_error_bag', $exception->errorBag)` as the bag name, so the submitted value wins. It only affects the redirect path; JSON responses ignore bags.
  • Do named error bags change the 422 JSON response for an API request?
    No. The JSON path returns `message` and `errors` from the exception and never reads the bag name. Bags exist only in the session flash that backs the redirect-and-redisplay flow for server-rendered forms.

saying these in an interview costs you the question

  • $errors->any() returns true when any named bag holds messages.
  • Reading a bag that was never flashed throws an exception, so wrap it in isset().
  • @error('login.email') reads the email message from the login bag.
  • The bag name becomes the top-level key of the 422 JSON error body.
  • The #[ErrorBag] attribute also works on Laravel 12 form requests.