My app was working perfectly on my local test environment. But on the live site, clicking on any idea showed the correct page for a moment, then a 404-style error page loaded over the top.
“things are working fine on my local test environment, but on live now when i click on an idea i see the correct idea but then what looks like a 404 error page loads over the top”
What followed was a multi-layered debugging session. I fixed one bug and another appeared. Then another. Three separate root causes were stacked on top of each other, all manifesting as the same visible error.
Table of contents
Open Table of contents
Bug 1: The null-safe operator
The first crash happened when loading an idea whose goal had been soft-deleted. The code tried to access the goal’s name:
// This crashes when $this->idea->goal is null
$this->goal_name = $this->idea->goal->name
?? auth()->user()->currentTeam->goal->name;
The ?? (null coalescing) operator looks like it handles this — “if the left side is null, use the right side.” But it doesn’t work the way you’d expect here.
The problem: $this->idea->goal returns null (the goal was soft-deleted). Then ->name tries to access a property on null, which is a fatal error. The fatal error happens before the ?? operator gets a chance to evaluate.
The fix
PHP’s null-safe operator (?->) stops the chain if any part is null:
// Before: crashes when goal is null
$this->goal_name = $this->idea->goal->name
?? auth()->user()->currentTeam->goal->name;
// After: returns null safely, then falls back
$this->goal_name = $this->idea->goal?->name
?? auth()->user()->currentTeam->goal->name;
The ?-> after goal says “if this is null, stop here and return null.” Then the ?? kicks in and uses the fallback value.
| Operator | What it does | When it helps |
|---|---|---|
?? | Returns right side if left side is null | When the value might be null |
?-> | Stops the chain if the object is null | When an intermediate object might be null |
Bug 2: Alpine x-init fires on page load
With bug 1 fixed, a new error appeared. The browser console showed a Livewire POST request returning 500 every time an idea page loaded.
“I didn’t think there was any HypothesisAiChat on pages like this one? I certainly can’t see anything that uses AI here”
The issue was in the Share modal. The component pre-set a selectedItem on mount:
// In the mount() method
$this->selectedItem = $this->idea;
This caused the Share modal’s template to render immediately (because @if($this->selectedItem) was now true). The modal had an x-init="generateLink()" directive — Alpine’s x-init fires as soon as the DOM element renders, not when the modal opens.
So on every page load, the Share modal was rendering in the background, immediately calling generateLink(), which fired a Livewire server request. This request was failing because of another unrelated issue, producing the 500 error.
The fix
Remove the pre-set and load on demand instead:
// Before: pre-sets on mount, causes x-init to fire
public function mount() {
$this->selectedItem = $this->idea;
}
// After: removed from mount, loaded on demand
// The menu item now calls selectItemForModal first:
<flux:menu.item
x-on:click="$wire.selectItemForModal({{ $idea->id }})
.then(() => $flux.modal('share-modal').show())">
Share
</flux:menu.item>
This means the modal only renders its content when the user actually clicks Share — not on every page load.
The lesson: x-init in Alpine.js fires when the element enters the DOM, not when it becomes visible. If you pre-render a modal’s content, any x-init directives inside it will fire immediately on page load.
Bug 3: toArray() vs all() on Laravel collections
The third bug was the deepest. A dependency update (bump deps) had upgraded the Prism PHP library. In the new version, Message objects implemented Laravel’s Arrayable interface.
The code was normalizing messages before passing them to the AI service:
// Before: converts Message objects back to plain arrays
return collect($messages)
->filter(fn ($message) => !isset($message['hidden']))
->values()
->toArray();
// After: preserves Message objects
return collect($messages)
->filter(fn ($message) => !isset($message['hidden']))
->values()
->all();
The difference between toArray() and all() on a Laravel Collection:
| Method | What it does |
|---|---|
toArray() | Recursively converts all items. If an item implements Arrayable, it calls toArray() on that item too. |
all() | Returns the raw items without any conversion. Objects stay as objects. |
Before the upgrade, Message objects didn’t implement Arrayable, so toArray() left them alone. After the upgrade, toArray() converted them back to plain arrays — and the AI service expected Message objects, not arrays.
The lesson: toArray() is not the same as all(). If your collection contains objects that implement Arrayable, toArray() will convert them. Use all() when you need to preserve the original object types.
Why this was hard to find
Each bug masked the next. The visible symptom — a 404-style error overlaying the page — could have been any of the three. Fix one, and the same symptom persisted because the next bug was still there.
This is common in production. Local environments are forgiving: goals aren’t soft-deleted, the AI service isn’t running, and dependencies might not be at the same version. Production surfaces edge cases that local development hides.
How to approach layered bugs
- Fix the first error you find. Don’t try to find all bugs at once. Fix the most obvious one and see if the symptom changes.
- Check the server logs, not just the browser. The browser showed a 404. The server logs showed a 500 from a completely different component.
- Question your assumptions. I was certain the idea page didn’t use AI. It didn’t — but a pre-rendered modal did, invisibly.
- Watch for dependency upgrades. A
composer updatecan change how existing code behaves without changing any of your files. The PrismArrayableaddition was a breaking change that looked like a feature.
Key takeaway
A single visible error can hide multiple bugs stacked on top of each other. Fix them one at a time, check the server logs after each fix, and question whether something invisible is running on page load. The three PHP patterns worth remembering: use ?-> instead of ?? when objects might be null, avoid x-init in pre-rendered modals, and use all() instead of toArray() when you need to preserve object types in collections.