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.

Solutions

  1. Verify the source string is complete before parsing (check stream end, file size, response length)
  2. For streaming/incomplete input use parseStreamingJson, which auto-closes truncated objects
  3. Re-request or re-read the payload if it was truncated by a provider or transport
  4. 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

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


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

Appendix: 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)