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

  1. Read the ERR: message embedded in the thrown error — it is the bundle's own diagnostic.
  2. Validate mermaid/excalidraw source before calling (JSON.parse for excalidraw is already done upstream; add a mermaid lint step).
  3. Ensure the reset contract holds: after any render error, loadBundle() must run before the next fence.
  4. 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

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


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/672615668404945b. Report an issue: GitHub.