{"record":{"id":"18bc96c27689378a","repo":"toon-format/toon","slug":"invalid-escape-sequence-u-hex-is-a-lone-surrog","errorCode":null,"errorMessage":"Invalid escape sequence: \\u${hex} is a lone surrogate. Supplementary code points MUST appear as literal UTF-8","messagePattern":"Invalid escape sequence: \\\\u(.+?) is a lone surrogate\\. Supplementary code points MUST appear as literal UTF-8","errorType":"exception","errorClass":"SyntaxError","httpStatus":null,"severity":"error","filePath":"packages/toon/src/shared/string-utils.ts","lineNumber":94,"sourceCode":"        i += 2\n        continue\n      }\n      if (next === DOUBLE_QUOTE) {\n        unescaped += DOUBLE_QUOTE\n        i += 2\n        continue\n      }\n      if (next === 'u') {\n        if (i + 6 > value.length) {\n          throw new SyntaxError(`Invalid escape sequence: truncated \\\\u escape at \"${value.slice(i, i + 6)}\"`)\n        }\n        const hex = value.slice(i + 2, i + 6)\n        if (!/^[0-9a-f]{4}$/i.test(hex)) {\n          throw new SyntaxError(`Invalid escape sequence: \\\\u must be followed by 4 hex digits, got \"${hex}\"`)\n        }\n        const codeUnit = Number.parseInt(hex, 16)\n        if (codeUnit >= 0xD800 && codeUnit <= 0xDFFF) {\n          throw new SyntaxError(`Invalid escape sequence: \\\\u${hex} is a lone surrogate. Supplementary code points MUST appear as literal UTF-8`)\n        }\n        unescaped += String.fromCodePoint(codeUnit)\n        i += 6\n        continue\n      }\n\n      throw new SyntaxError(`Invalid escape sequence: \\\\${next}`)\n    }\n\n    unescaped += value[i]\n    i++\n  }\n\n  return unescaped\n}\n\n/** Finds the index of the closing double quote, accounting for escape sequences. */\nexport function findClosingQuote(content: string, start: number): number {","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/toon-format/toon/blob/604eac266e35166bed6f5b9e3bd586e9c60a9330/packages/toon/src/shared/string-utils.ts#L76-L112","documentation":"TOON deliberately forbids \\u escapes in the surrogate range D800–DFFF (lone surrogates). Supplementary code points must be written as literal UTF-8 characters. This avoids producing invalid strings and normalizes representation.","triggerScenarios":"Parsing a TOON string containing \\uD800–\\uDFFF (e.g. \"\\uD83D\" without its low surrogate, or any lone surrogate code).","commonSituations":"Converting JSON that used surrogate pairs (\\uD83D\\uDE00) into TOON; tools that escape astral characters as surrogate pairs rather than emitting UTF-8.","solutions":["Replace the surrogate-pair escape with the literal UTF-8 emoji/character (e.g. 😀)","Re-encode the source using a converter that emits UTF-8 instead of surrogate escapes","Combine surrogate pairs correctly before conversion if migrating from JSON"],"exampleFix":"// before (TOON input)\nemoji: \"\\uD83D\\uDE00\"\n// after\nemoji: \"😀\"","handlingStrategy":"validation","validationCode":"function hasLoneSurrogateEscapes(s: string): boolean {\n  const hexes = [...s.matchAll(/\\\\u([dD][89abAB][0-9a-fA-F]{2}|[dD][cdefCDEF][0-9a-fA-F]{2})/g)].map(m => parseInt(m[1], 16))\n  return hexes.some(h => h >= 0xD800 && h <= 0xDFFF)\n}","typeGuard":null,"tryCatchPattern":"try {\n  const value = decode(input)\n} catch (e) {\n  if (e instanceof SyntaxError && e.message.includes('lone surrogate')) {\n    // convert surrogate pairs to real characters before decoding\n  }\n  throw e\n}","preventionTips":["Convert JSON surrogate-pair escapes to literal UTF-8 during migration","Ensure your toolchain treats all text as UTF-8 end to end","Reject lone surrogates in your own input pipeline before they reach TOON"],"tags":["decoder","unicode","surrogate","escape-sequence"],"backgroundTag":"lone-surrogate-escape","analyzedSha":"604eac266e35166bed6f5b9e3bd586e9c60a9330","analyzedAt":"2026-08-31T11:09:25.043Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}