can1357/oh-my-pi · error · SyntaxError
Unterminated object
Error message
Unterminated object
What it means
Thrown by RelaxedJson.#object in strict mode when the input ends (this.#i >= this.#n) while still inside an object — the parser reached end-of-input before seeing the closing '}'. In partial (streaming) mode this is not an error; the object is auto-closed with what was parsed so far. In strict mode it throws so truncated JSON never passes as final.
Source
Thrown at packages/utils/src/json-parse.ts:259
if (c === '"' || c === "'") return this.#string(s.charCodeAt(this.#i));
const cc = s.charCodeAt(this.#i);
if (cc === 0x2d /* - */ || cc === 0x2b /* + */ || cc === 0x2e /* . */ || (cc >= 0x30 && cc <= 0x39)) {
// JS-only NaN / Infinity are deliberately not accepted: a tool must not
// execute with a non-finite numeric arg; they fall through #number's
// NaN guard (strict throw / partial rollback) like other bad tokens.
return this.#number();
}
return this.#keyword(allowBareword);
}
#object(): Record<string, unknown> {
this.#i++; // consume {
const out: Record<string, unknown> = {};
for (;;) {
this.#ws();
if (this.#i >= this.#n) {
if (this.#partial) return out;
throw new SyntaxError("Unterminated object");
}
const c = this.#s[this.#i];
if (c === "}") {
this.#i++;
return out;
}
if (c === ",") {
// Tolerate leading / doubled / trailing commas.
this.#i++;
continue;
}
const key = this.#key();
this.#ws();
if (this.#i < this.#n && this.#s[this.#i] === ":") {
this.#i++;
} else if (this.#partial) {
return out;
} else {View on GitHub (pinned to 9690622007)
Solutions
- Verify the source string is complete before parsing (check stream end, file size, response length)
- For streaming/incomplete input use parseStreamingJson, which auto-closes truncated objects
- Re-request or re-read the payload if it was truncated by a provider or transport
- Use classifyJsonPrefix: 'prefix' means the input can still be completed, so wait for more data
Example fix
// before
const v = parseJsonWithRepair(`{"path": "x.ts"`); // SyntaxError
// after: tolerate a truncated streaming buffer
const v = parseStreamingJson(`{"path": "x.ts"`); // {path: 'x.ts'} Defensive patterns
Strategy: validation
Validate before calling
import { classifyJsonPrefix } from "@oh-my-pi/pi-utils/json-parse";
// 'complete' => safe for parseJsonWithRepair; 'prefix' => wait for more data or use parseStreamingJson
const state = classifyJsonPrefix(text); Type guard
function isCompleteJson(text: string): boolean {
return classifyJsonPrefix(text) === "complete";
} Try / catch
try {
const value = parseJsonWithRepair<T>(text);
} catch (err) {
if (err instanceof SyntaxError && err.message === "Unterminated object") {
const partial = parseStreamingJson<T>(text); // best-effort auto-closed value
} else throw err;
} Prevention
- Never final-parse until the stream/reader signals completion
- Use parseStreamingJson for incremental buffers and parseJsonWithRepair only for final parses
- Verify file size / content-length against bytes actually received
- Use classifyJsonPrefix to distinguish 'prefix' (wait) from 'invalid' (fail fast)
When it happens
Trigger: parseJsonWithRepair with a truncated object: '{"a": 1', '{"a":', or '{'. Any place end-of-input occurs inside an object body when this.#partial is false.
Common situations: LLM tool-call arguments cut off by a token/size limit; reading a file or network response before it fully arrived; log truncation dropping the tail of a JSON payload.
Related errors
- Unterminated array
- Unterminated string
- Expected value after ':'
- Unexpected trailing characters at position ${this.#i}
- Expected ':' in object
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/a61b42ff0788059e.
Report an issue: GitHub.