{"record":{"id":"3dc4306573ba049c","repo":"can1357/oh-my-pi","slug":"unexpected-token-at-position-this-i","errorCode":null,"errorMessage":"Unexpected token at position ${this.#i}","messagePattern":"Unexpected token at position (.+?)","errorType":"exception","errorClass":"SyntaxError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/json-parse.ts","lineNumber":504,"sourceCode":"\t#keyword(allowBareword: boolean): unknown {\n\t\tconst s = this.#s;\n\t\tconst i = this.#i;\n\t\tfor (const [word, value] of KEYWORDS) {\n\t\t\t// Require a non-identifier boundary so `Truex` / `nullish` are not misread\n\t\t\t// as the keyword followed by junk.\n\t\t\tif (s.startsWith(word, i) && !isIdentChar(s.charCodeAt(i + word.length))) {\n\t\t\t\tthis.#i += word.length;\n\t\t\t\treturn value;\n\t\t\t}\n\t\t}\n\t\tif (this.#partial) {\n\t\t\t// Incomplete / unrecognized atomic token at the streaming edge — signal the\n\t\t\t// caller to roll back to the last valid prefix instead of committing junk.\n\t\t\tthis.#i = this.#n;\n\t\t\treturn INCOMPLETE;\n\t\t}\n\t\tif (allowBareword) return this.#bareword();\n\t\tthrow new SyntaxError(`Unexpected token at position ${this.#i}`);\n\t}\n\n\t/**\n\t * Strict-mode recovery of an unquoted string value, e.g.\n\t * `{\"paths\": packages/foo/*}`: consume until `,` / `}` / `]` / newline and\n\t * trim trailing whitespace. Recovery still throws — so a final parse never\n\t * accepts a half-formed or non-finite argument — when the token:\n\t * - hits end-of-input before a delimiter (truncated value);\n\t * - contains a `\"`, `{`, `[`, or a key-like `:` — this parser accepts\n\t *   unquoted keys, so a missed comma (`{\"a\": foo \"b\": 1}`, `{a: foo b: 1}`)\n\t *   would otherwise silently swallow the following field. A colon followed\n\t *   by `/` or `\\` stays literal so URL and Windows-path values recover;\n\t * - is a non-finite atom ({@link NON_RECOVERABLE_BAREWORDS}).\n\t */\n\t#bareword(): string {\n\t\tconst s = this.#s;\n\t\tconst start = this.#i;\n\t\tlet i = start;","sourceCodeStart":486,"sourceCodeEnd":522,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/json-parse.ts#L486-L522","documentation":"This SyntaxError is thrown by the streaming JSON parser's #keyword routine when it encounters an unrecognized atomic token at the current position. Unlike incomplete-token conditions (which return INCOMPLETE so the caller can roll back), this is a definitive syntax failure — the input contains a character that cannot start any JSON value, keyword, or recoverable bareword. The parser is designed for incremental/streaming parsing, so 'position' refers to the character offset within the chunk being parsed.","triggerScenarios":"Calling the parser with input whose next token starts with a character that is not a quote, digit, '{', '[', a recognized keyword letter, or a valid bareword start while parsing a value (via #value). E.g. feed '{\"a\": ?}' or '{\"a\": +x}' — '?'/'+' cannot begin a token, so #keyword throws immediately instead of returning INCOMPLETE.","commonSituations":"Truncated LLM/CLI output containing stray characters, hand-edited JSON with typos (unquoted punctuation, stray commas followed by garbage), binary bytes or BOM/preamble before JSON, or users trying to parse pseudo-JSON like {'a': undefined} or JSON5 syntax the parser does not support.","solutions":["Inspect the input at the reported position and remove/fix the offending character (print the substring around `position`).","Ensure the text is valid JSON before parsing (validate with JSON.parse or a linter).","If input may arrive incrementally, make sure the parser's INCOMPLETE return is handled by rolling back to the last valid prefix instead of treating it as fatal.","If the value was meant to be an unquoted string (e.g. {\"paths\": packages/foo/*}), quote it properly or rely on strict-mode recovery only for supported bareword shapes.","Strip BOMs, log prefixes, or markdown fences before parsing."],"exampleFix":"// before\nparse('{\"a\": ?}'); // SyntaxError: Unexpected token at position 6\n// after\nparse('{\"a\": null}'); // OK — or handle INCOMPLETE and wait for more input in streaming mode","handlingStrategy":"try-catch","validationCode":"// quick sanity check before parsing\ntypeof input === \"string\" || throw new TypeError(\"input must be a string\");\nconst first = input.trimStart()[0];\nif (first !== undefined && !'{[\"'.includes(first) && !/[\\-0-9tfn]/.test(first)) {\n  throw new SyntaxError(`Input does not start with a JSON token: ${JSON.stringify(first)}`);\n}","typeGuard":"function looksLikeJsonText(s: unknown): s is string {\n  return typeof s === \"string\" && /^[\\s]*[\\[{\"\\-0-9tfn]/.test(s);\n}","tryCatchPattern":"try {\n  return parser.feed(chunk);\n} catch (err) {\n  if (err instanceof SyntaxError && err.message.startsWith(\"Unexpected token at position\")) {\n    const pos = Number(err.message.match(/position (\\d+)/)?.[1]);\n    logger.warn(\"JSON syntax error\", { pos, context: chunk.slice(Math.max(0, pos - 20), pos + 20) });\n    return rollBackToLastValidPrefix();\n  }\n  throw err;\n}","preventionTips":["Always handle the parser's INCOMPLETE signal by rolling back rather than forcing a parse of partial data.","Validate payloads with JSON.parse or a linter in tests/CI before they reach the streaming parser.","Strip BOMs, log prefixes, and markdown code fences from upstream text.","When debugging, print a window of characters around the reported position."],"tags":["json","syntax-error","parsing","streaming"],"backgroundTag":"invalid-json-syntax","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}