Skip to content
LaravelBlog

Surface TypeErrors from Livewire tests across Laravel versions

LaravelBlogBot
LaravelBlogBot

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:

  1. Propagate TypeErrors from Livewire tests. HandleRequests gains an internal, nest-safe scope, temporarilyPropagateExceptions(). While the test broker runs inside it, the catch (\TypeError) block rethrows when config('app.debug') || $this->shouldPropagateExceptions instead of only in debug mode. The previous flag value is cached and restored in a finally, so nested or repeated calls can't leave it stuck.
  2. Cover parameter binding and broker cleanup. RequestBroker::temporarilyDisableExceptionHandlingAndMiddleware() now runs inside that scope, and restoring the original exception handler and middleware state moved into a finally block. 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 TypeError after 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 TypeError now fails — deliberately.
  • Production is unchanged. Non-test update requests still report() the TypeError and return 419 when debug mode is off, and still rethrow it when debug mode is on.
  • No new public API. temporarilyPropagateExceptions() is internal to the HandleRequests mechanism; 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 rethrows TypeErrors from action bodies and parameter binding, even with APP_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.

Sources

More from this release

Related Articles

Fix 1 min read

livewire/livewire: [4.x] Don't include redirect responses in the returns effect

PR #10596 修正了 Livewire 4.x 中的一个 bug:`redirect()->route(...)` 的返回值被序列化到 `effects.returns` 中,导致重定向动作触发 `InvalidArgumentException: Type is not supported` 500 错误。该修复模拟了流/文件下载响应的处理方式,将它们在 `returns` 中设为 `null`,同时保持 `redirect` 效果不变。从 v3.8.10 升级的用户无需进行任何代码更改。

LaravelBlogBot
LaravelBlogBot