Finding the actual failure
A Prisma PHP request crosses three boundaries: PHP render, the PulsePoint compiler, and the browser runtime. Each one reports failure somewhere different. This page is a map of where to look.
Server-side errors
Uncaught exceptions are captured by ErrorHandler and
rendered through the nearest error.php.
Whether you see the real message or a generic one is controlled by SHOW_ERRORS in
.env.
# Show real exception messages instead of a generic one.
# Local development only - never enable this in production.
SHOW_ERRORS=true
echo and var_dump from inside an
#[Exposed] function do not appear on the page — that request is not rendering the page.
They land in the RPC response body, which you read in the browser's Network tab.
For a page render, an echo before the route root is emitted outside the boundary and can break the
single-root rule; prefer returning the value and rendering it, or write to the PHP error log.
Debugging an RPC call
pp.rpc(...) rejects on framework-level failures, so the
fastest way to see what went wrong is a try/catch plus the response status in the Network tab.
<script>
async function debugCall() {
try {
const response = await pp.rpc('getReport', { id: 42 });
// An aborted call resolves - it does not reject.
if (response?.cancelled) return console.warn('cancelled');
console.log('resolved with', response);
} catch (error) {
// "Authentication required" / "Permission denied" /
// "Rate limit exceeded" / the InvalidArgumentException message
console.error('rejected with', error.message);
}
}
</script>
| 404 | The name is wrong, the file holding the function is not loaded for this route, or you
forgot #[Exposed]. All three look identical, on purpose. |
| 403 | Origin, CSRF cookie, or role mismatch. The client message is always the same “Permission denied”; the real reason is only in the response body. |
| 429 | Rate limited — or APCu is missing. If every call returns 429, check for the APCu extension before debugging your code. |
| 415 | The body was neither JSON nor multipart. Usually a hand-built request rather than
pp.rpc. |
| 500 | The function threw. Turn on SHOW_ERRORS to get the
real message instead of the generic one. |
InvalidArgumentException is the one sanctioned way to send a message across:
it reaches the caller as a 400 with the message intact. Everything else routine — field errors, availability checks — should be
returned as data, not thrown.
Reading PulsePoint console output
| Message | What it usually means |
|---|---|
| [PP-ERROR] Failed to eval "..." | A template expression referenced a name the component scope does not have. Nine times out of ten the declaration is nested inside a function or block, so it was never exported to the template. |
| [PP-WARN] Invalid template child | An expression evaluated to an object, function, or symbol. Those render as an empty string. Interpolate a property, not the object. |
| ... returned a Promise | An async callback was passed to
pp.effect. The returned promise is discarded, so the cleanup never runs. Call the async
function from inside a sync effect instead. |
| pp is not defined | A component script executed before the runtime module loaded — which means the page's component boundaries were not deferred. See the note below. |
| RPC returned a stream, but no onStream handler provided. | The exposed function yields. Move the UI updates into
onStream; the promise resolves after the stream ends with no final payload. |
Escaped markup outside a code element breaks the whole page. The compiler only re-escapes text whose direct parent is
code, pre, samp,
kbd or var. An
<div> inside a plain <div> or
<td> is decoded back into a real tag, which unbalances the element scan and silently disables
component deferral for the entire page.
The symptom is a burst of pp is not defined and duplicate-declaration errors pointing at the
layout, not at the page that caused it. To confirm, view source and check that the layout root is wrapped in
<template pp-component="...">. The fix is to wrap the escaped snippet in
<code>.
Render profiling
The runtime can collect per-render timings. Enable it from the console at any time, or before the first paint when you need the mount phase itself.
// after load
pp.enablePerf();
// ... interact with the page ...
pp.getPerfStats();
pp.resetPerfStats();
pp.disablePerf();
// to profile the mount phase, set this first and reload
localStorage["pp-perf"] = "1";
Before you debug the code
Check the URL
The dev proxy port is not always 3000. settings/bs-config.json holds the live
local, external and UI URLs for this app.
Check the feature flag
Websocket, MCP, TypeScript, Swagger and Prisma scaffolds only exist when enabled in
prisma-php.json. A missing class is often a disabled feature.
Check the generated ORM
Lib\Prisma\Classes\Prisma does not exist until
npx ppo generate has run after a migration.