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
- Trim and inspect the input at the reported position to find the stray characters
- Split concatenated JSON values (e.g. NDJSON / sibling tool-call buffers) and parse only the first value, or parse each separately
- If the caller wants prefix-tolerant behavior for incomplete input, use parseStreamingJson instead of parseJsonWithRepair
- 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
- Parse each JSON value separately instead of concatenating buffers
- Use classifyJsonPrefix before final-parsing streamed buffers
- Check the reported position in the message to pinpoint the stray characters
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
- Expected ':' in object
- Expected ',' or '}' in object
- Expected ',' or ']' in array
- Expected object key
- Invalid number: ${token}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/27c614215c130fb4.
Report an issue: GitHub.