Surface TypeErrors thrown during Livewire tests (#10407)
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:
- 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 tocall(). Fix the component or the call site; do not paper over the failure. - 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
WithoutExceptionHandlingHandleris active (exception handling disabled, as inLivewire::test()) or whenapp.debugis 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).