can1357/oh-my-pi · error · SyntaxError

Unexpected trailing characters at position ${this.#i}

Error message

Unexpected trailing characters at position ${this.#i}

What it means

This error is thrown by RelaxedJson.parse (strict mode) when, after successfully parsing one complete JSON value, non-whitespace characters remain at the end of the input. The relaxed parser tolerates many LLM malformations, but it deliberately refuses trailing garbage in strict mode so a final parse never silently accepts a malformed, doubled, or half-formed document.

Source

Thrown at packages/utils/src/json-parse.ts:204

	#i = 0;

	constructor(source: string, partial: boolean) {
		this.#s = source;
		this.#n = source.length;
		this.#partial = partial;
	}

	parse(): unknown {
		this.#ws();
		if (this.#i >= this.#n) {
			if (this.#partial) return undefined;
			throw new SyntaxError("Unexpected end of JSON input");
		}
		const value = this.#value(false);
		if (value === INCOMPLETE) return undefined;
		this.#ws();
		if (!this.#partial && this.#i < this.#n) {
			throw new SyntaxError(`Unexpected trailing characters at position ${this.#i}`);
		}
		return value;
	}

	#ws(): void {
		const s = this.#s;
		for (;;) {
			while (this.#i < this.#n && isWhitespace(s.charCodeAt(this.#i))) this.#i++;
			if (this.#i + 1 < this.#n && s.charCodeAt(this.#i) === 0x2f /* / */) {
				const next = s.charCodeAt(this.#i + 1);
				if (next === 0x2f /* / line comment */) {
					this.#i += 2;
					while (this.#i < this.#n && s.charCodeAt(this.#i) !== 0x0a) this.#i++;
					continue;
				}
				if (next === 0x2a /* * block comment */) {
					this.#i += 2;
					while (

View on GitHub (pinned to 9690622007)

Solutions

  1. Trim and inspect the input at the reported position to find the stray characters
  2. Split concatenated JSON values (e.g. NDJSON / sibling tool-call buffers) and parse only the first value, or parse each separately
  3. If the caller wants prefix-tolerant behavior for incomplete input, use parseStreamingJson instead of parseJsonWithRepair
  4. Use classifyJsonPrefix to detect a second top-level value or trailing garbage before parsing

Example fix

// before: two values glued together
const v = parseJsonWithRepair<{a:number}>(`{"a":1}{"b":2}`); // throws
// after: parse the first value only
const text = `{"a":1}{"b":2}`;
const v = parseJsonWithRepair<{a:number}>(text.slice(0, 7)); // {a: 1}
Defensive patterns

Strategy: try-catch

Validate before calling

import { classifyJsonPrefix } from "@oh-my-pi/pi-utils/json-parse";
if (classifyJsonPrefix(text) === "complete") {
  // safe to final-parse
}

Type guard

function isCompleteSingleValue(text: string): boolean {
  try { JSON.parse(text); return true; } catch { return false; }
}

Try / catch

try {
  const value = parseJsonWithRepair<T>(text);
} catch (err) {
  if (err instanceof SyntaxError && err.message.startsWith("Unexpected trailing characters")) {
    // split concatenated values or slice to the first complete value
  } else throw err;
}

Prevention

When it happens

Trigger: Calling parseJsonWithRepair(json) (strict mode) with input containing extra content after the first value, e.g. two concatenated objects '{"a":1}{"b":2}', a stray '}' or prose after the value, or a truncated suffix like '{"a":1}]'. Also fired when a position is reported, so this.#i names the byte offset where the trailing characters start.

Common situations: Concatenating multiple LLM tool-call argument chunks without checking whether the first was already a complete value; a provider streaming two sibling tool calls into one buffer; log or template output with debug text appended after the JSON payload.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/27c614215c130fb4. Report an issue: GitHub.