Surface TypeErrors from Livewire tests across Laravel versions
Contributed by calebporzio
Part of the livewire v4.4.7 release
If your test suite runs with APP_DEBUG=false — as most CI pipelines do — PR #10621 fixes a bug that could hide real bugs in your components. Previously, when a component action threw a TypeError (or PHP threw one while binding action parameters), Livewire::test() with debug mode disabled returned an empty, 419-backed component response instead of surfacing the exception. The original error and its stack trace were swallowed, so a genuine TypeError in your code looked like a testing-infrastructure quirk. This change, which fixes #10385 and lands on the 4.x branch, ensures TypeErrors are always rethrown to PHPUnit during Livewire tests across every supported Laravel version, while production behavior stays exactly the same.
Why TypeErrors were getting swallowed
Livewire::test() works by temporarily disabling exception handling (except for HttpException and AuthorizationException) and middleware, so that anything thrown during a component update bubbles up to PHPUnit. The gap was inside Livewire's own request pipeline: HandleRequests::handleUpdate() wraps updates in a catch (\TypeError) block. It reports the error, rethrows it only when config('app.debug') is true, and otherwise calls abort(419).
That abort() throws an HttpException — precisely the type the test broker intentionally renders. So the synthesized 419 short-circuited the request, the subsequent render completed with empty component state, and Laravel's exception-rethrowing test handler never saw the original TypeError.
The debug flag made this especially confusing: locally with APP_DEBUG=true the TypeError surfaced, but in CI with APP_DEBUG=false it was masked.
What changed
The fix landed in two levels:
- Propagate TypeErrors from Livewire tests.
HandleRequestsgains an internal, nest-safe scope,temporarilyPropagateExceptions(). While the test broker runs inside it, thecatch (\TypeError)block rethrows whenconfig('app.debug') || $this->shouldPropagateExceptionsinstead of only in debug mode. The previous flag value is cached and restored in afinally, so nested or repeated calls can't leave it stuck. - Cover parameter binding and broker cleanup.
RequestBroker::temporarilyDisableExceptionHandlingAndMiddleware()now runs inside that scope, and restoring the original exception handler and middleware state moved into afinallyblock. Before, if an exception escaped the callback, the cleanup lines were skipped entirely, leaving the app with exception handling disabled and middleware skipped for the rest of the test run.
Notably, the implementation does not use Laravel's WithoutExceptionHandlingHandler marker — the approach taken by the earlier #10407 — because that marker isn't available on the supported Laravel 10. This Livewire-owned scope works across the declared Laravel 10–13 range without adding a public API.
What it looks like in practice
Consider a component with a type-unsafe action:
class SaveSettings extends Component
{
public function save()
{
array_merge(collect(), []); // bug: Collection passed where array expected
}
public function saveItems(array $items)
{
// ...
}
}
With APP_DEBUG=false, this test previously ran without any exception — you got an empty, 419-backed component response, and any assertion depending on real component state failed with a misleading message:
// Before: no exception thrown, original TypeError lost
Livewire::test(SaveSettings::class)->call('save');
After the fix, PHPUnit fails with the real error:
TypeError: array_merge(): Argument #1 must be of type array,
Illuminate\Support\Collection given
TypeErrors raised during action parameter binding are covered too, since PHP throws those before your action body ever runs:
Livewire::test(SaveSettings::class)->call('saveItems', 'not-an-array');
// TypeError: saveItems(): Argument #1 ($items) must be of type array, string given
Upgrade impact
If you're upgrading from v3.8.10, be aware that v3 had the same masking behavior — this PR changes what your test suite reports, not what your components do.
- Newly rethrown TypeErrors are real bugs. If a test starts failing with a
TypeErrorafter upgrading, the component had that bug all along; the suite simply couldn't see it with debug mode off. Fix the component (or the call signature in your test), not the harness. - Tests that depended on swallowed exceptions will fail. Any test that implicitly relied on receiving an empty 419-backed response after a
TypeErrornow fails — deliberately. - Production is unchanged. Non-test update requests still
report()theTypeErrorand return 419 when debug mode is off, and still rethrow it when debug mode is on. - No new public API.
temporarilyPropagateExceptions()is internal to theHandleRequestsmechanism; no application code changes are required. - Cleaner test isolation. Because cleanup now happens in
finally, a failing Livewire test no longer leaks disabled exception handling or skipped middleware into subsequent tests.
Takeaways
Livewire::test()now rethrowsTypeErrors from action bodies and parameter binding, even withAPP_DEBUG=false.- The 419-masking bug (#10385) is fixed via a Livewire-internal propagation scope that works on Laravel 10 through 13 without framework-specific markers.
- Production behavior is untouched: report plus 419 with debug off, rethrow with debug on.
- Exception-handler and middleware state is now restored in
finally, preventing cross-test state leaks after an exception escapes. - No application code changes required — but expect previously hidden TypeErrors to surface in CI.