garrytan/gstack · error · RenderCallError
${bundleErrorMessage}
Error message
${bundleErrorMessage} What it means
RenderCallError thrown by RenderTab.call() when the bundle-side async function (e.g. __renderMermaid, __excalidrawToSvg) rejects, or returns a value without the expected OK:/ERR: protocol prefix. The bundle reports failures as 'ERR:<message>' so a render failure is data, not a browse exit; this error re-throws that message.
Source
Thrown at make-pdf/src/diagram-prepass.ts:387
`(${READY_TIMEOUT_MS}ms). Check \`browse js "window.__errors"\` on tab ${this.tabId}.`,
);
}
}
/**
* Call one of the bundle's async window functions with JSON-safe string
* args. Errors come back as a recognizable ERR: prefix so a render failure
* is data, not a thrown browse exit.
*/
call(fn: string, ...args: Array<string | number>): string {
const argList = args.map((a) => JSON.stringify(a)).join(",");
const expression =
`window.${fn}(${argList})` +
`.then(r => "OK:" + r)` +
`.catch(e => "ERR:" + String((e && e.message) || e))`;
const result = this.js(expression);
if (result.startsWith("OK:")) return result.slice(3);
if (result.startsWith("ERR:")) throw new RenderCallError(result.slice(4));
throw new RenderCallError(`unexpected bundle result: ${result.slice(0, 200)}`);
}
private js(expression: string): string {
// Large payloads (scene JSON, SVG text, data URIs) blow past argv limits —
// browseClient.js shells out with the expression as an argv element. The
// limit is BYTES, not chars (CJK content is 3x its char count in UTF-8),
// and Windows caps the whole command line at 32,767 chars — so anything
// big ships via `browse eval <file>` instead: one spawn, any size.
if (Buffer.byteLength(expression, "utf8") <= MAX_ARGV_EXPR_BYTES) {
return browseClient.js({ expression, tabId: this.tabId });
}
return this.jsViaFile(expression);
}
/** argv-safe path for big expressions: stage to a tmp file under browse's
* safe dirs and run `browse eval <file>` (one spawn regardless of size). */
private jsViaFile(expression: string): string {View on GitHub (pinned to 94993f7401)
Solutions
- Read the ERR: message embedded in the thrown error — it is the bundle's own diagnostic.
- Validate mermaid/excalidraw source before calling (JSON.parse for excalidraw is already done upstream; add a mermaid lint step).
- Ensure the reset contract holds: after any render error, loadBundle() must run before the next fence.
- Rebuild the bundle if the function name or signature changed (`bun run build:diagram-render`).
Example fix
// before
const result = this.js(expression);
if (result.startsWith('OK:')) return result.slice(3);
if (result.startsWith('ERR:')) throw new RenderCallError(result.slice(4));
throw new RenderCallError(`unexpected bundle result: ${result.slice(0, 200)}`);
// after: include the fence id and function in the error for traceability
const result = this.js(expression);
if (result.startsWith('OK:')) return result.slice(3);
if (result.startsWith('ERR:')) throw new RenderCallError(`${fn}: ${result.slice(4)}`);
throw new RenderCallError(`${fn}: unexpected bundle result: ${result.slice(0, 200)}`); Defensive patterns
Strategy: try-catch
Validate before calling
// Validate fence source before sending to the bundle.
function validateFence(fence: DiagramFence): string | null {
if (fence.lang === 'mermaid') {
if (/graph|sequenceDiagram|flowchart|classDiagram|stateDiagram|erDiagram/.test(fence.source)) return null;
return 'mermaid source does not start with a known diagram type';
}
try { JSON.parse(fence.source); return null; } catch { return 'excalidraw source is not valid JSON'; }
} Type guard
import { RenderCallError } from './diagram-prepass';
function isRenderCallError(e: unknown): e is RenderCallError {
return e instanceof RenderCallError;
} Try / catch
for (const fence of fences) {
try {
render(fence);
} catch (e) {
if (e instanceof RenderCallError) {
slots.set(fence.token, buildDiagnosticBlock(fence, e.message));
tab.loadBundle(); // reset contract before next fence
continue;
}
throw e;
}
} Prevention
- Always run loadBundle() after a RenderCallError before the next fence.
- Lint mermaid and JSON.parse excalidraw sources before invoking the bundle.
- Keep fence payloads small to avoid renderer OOM.
- Pin the diagram-render bundle to a version whose function names match the caller.
When it happens
Trigger: Calling tab.call('__renderMermaid', id, source) or tab.call('__excalidrawToSvg', source) where the bundle function throws (invalid mermaid syntax, unparseable excalidraw JSON, renderer OOM), or returns an unexpected string. Also if the bundle was poisoned by a prior fence and not reloaded.
Common situations: A mermaid fence with a syntax error the bundle rejects; excalidraw JSON missing a required field; a CJK/huge diagram exhausting renderer memory; a prior poisoned fence corrupting global state because loadBundle reset was skipped; a bundle version whose function signatures changed.
Related errors
- diagram-render bundle did not become ready in the browse tab
- diagram-render bundle not found. Tried: ${candidates.map((c)
- invalid mermaid render id: ${id}
- excalidraw scene has no elements array
- 127
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/672615668404945b.
Report an issue: GitHub.