Skip to content
LaravelBlog

Surface TypeErrors thrown during Livewire tests (#10407)

LaravelBlogBot
LaravelBlogBot

Contributed by joshhanley

Part of the livewire v4.4.7 release

If you have ever watched a Livewire::test() assertion fail with no obvious cause, this release explains why. Up to and including v3.8.10, a TypeError thrown inside a component action — or thrown while binding action parameters — was silently converted into a 419 response during tests. Your assertion failed, the original exception and its stack trace were nowhere to be found, and debugging turned into guesswork. Pull request #10407 fixes this for Livewire v4 (it resolves issue #10385): whenever exception handling is disabled, as it is under Livewire::test(), Livewire now rethrows the original TypeError instead of absorbing it. Production behaviour is completely unchanged.

Why the exception disappeared

Livewire deliberately catches TypeErrors while handling update requests. In production, that lets the framework report() the exception and return a 419 response — a graceful failure path when debug mode is off.

The trouble starts in tests. Livewire::test() disables Laravel's exception handling so that real exceptions bubble up to PHPUnit. Laravel implements that by swapping in a special handler, WithoutExceptionHandlingHandler, which rethrows anything it receives. But Livewire's own catch (\TypeError) block ran first: the exception never reached Laravel's testing handler, got converted into a 419 abort, and the test harness happily received a response. It then built an empty component state, and your assertion — assertHasErrors(), assertSet(), whatever — failed with zero mention of the actual error.

Consider this component action:

class EditMessage extends Component
{
    public function save(): void
    {
        array_merge(collect(), []); // TypeError: Collection given, array expected
    }
}

Before this fix, the following test simply reported a failed assertion:

Livewire::test(EditMessage::class)
    ->call('save')
    ->assertHasErrors(); // fails — and nothing tells you why

The fix: respect disabled exception handling

The change lives in HandleRequests.php and amounts to a single condition:

catch (\TypeError $e) {
    report($e);

    if (config('app.debug') || app(ExceptionHandler::class) instanceof WithoutExceptionHandlingHandler) {
        throw $e;
    }

    abort(419);
}

Previously the condition was only config('app.debug'). Now Livewire also checks whether the container's exception handler is Laravel's WithoutExceptionHandlingHandler — the marker that a test has disabled exception handling. If it is, the original TypeError is rethrown, exactly as you would expect from any uncaught exception under test.

The pull request adds two regression tests, and notably both run with app.debug explicitly set to false, proving the rethrow does not depend on debug mode. One covers a TypeError thrown from the action body; the other covers a TypeError thrown while binding action parameters — passing the wrong type to call().

What you will see after upgrading

Tests now surface the real error, with a full stack trace:

TypeError

array_merge(): Argument #1 must be of type array, Illuminate\Support\Collection given

And for parameter binding, this:

// public function saveItems(array $items)
Livewire::test(ComponentWithActionThatThrowsTypeError::class)
    ->call('saveItems', 'not-an-array');

now throws TypeError: ... must be of type array, string given and points straight at the offending call, instead of failing downstream assertions against an empty component state.

Production behaviour is unchanged

The new check only triggers under test conditions:

  • report($e) is still called before anything else.
  • With app.debug = true, the exception is thrown — same as v3.
  • With normal exception handling and debug off, Livewire still reports the error and returns 419.
  • There is no new configuration and no API change.

Upgrade impact from v3.8.10

There is nothing to migrate, but two practical consequences when you move to v4:

  1. Expect new red tests — and treat them as gifts. A suite that silently swallowed TypeErrors will now fail loudly. Those failures reveal genuine bugs: mismatched action signatures or wrong argument types passed to call(). Fix the component or the call site; do not paper over the failure.
  2. Deliberate 419 assertions still work. If a test intentionally exercises the 419 path, re-enable exception handling in that specific test so the request is processed as it would be in production, and the 419 status can be asserted like any other HTTP response.

In short:

  • TypeErrors thrown in component actions or during action-parameter binding are now rethrown during tests instead of being masked by a 419 response.
  • The rethrow triggers when Laravel's WithoutExceptionHandlingHandler is active (exception handling disabled, as in Livewire::test()) or when app.debug is on.
  • Production behaviour is untouched: report the error, then 419 with debug off.
  • After upgrading from v3.8.10, previously "passing" tests may fail with real TypeErrors — fix the underlying type bugs rather than the tests.
  • Categories: fix (3), upgrade notes (11).

Sources

More from this release

Related Articles