apple/pkl · error · ParserError

invalidUnicodeEscapeSequence

invalidUnicodeEscapeSequence

Error message

Invalid Unicode escape sequence `{text}`.

Valid Unicode escape sequences are {prefix}'{'0'}' to {prefix}'{'10FFFF'}' (1-6 hexadecimal characters).

What it means

Pkl string escapes use the form `\u{XXXXXX}` with 1–6 hexadecimal digits (code points up to 10FFFF). The parser decodes the escape by parsing the hex substring between braces with Integer.parseInt(radix 16); if that fails (invalid characters, empty body, or a code point out of range caught as NumberFormatException) it throws invalidUnicodeEscapeSequence, echoing the full offending text and the escape prefix, at the token's span.

Solutions

  1. Fix the escape to 1–6 hexadecimal digits inside braces, e.g. `\u{1F600}`
  2. Convert non-hex characters to valid hex digits (e.g. use `\u{41}` instead of `\u{G}`)
  3. Ensure the code point is ≤ 10FFFF; split surrogate pairs into single code points
  4. If migrating from Java/JSON, transform `\uXXXX` to Pkl's braced form `\u{XXXX}`

Example fix

// before
s = "\u{12G4}"
// after
s = "\u{12F4}"
Defensive patterns

Strategy: validation

Validate before calling

function validPklUnicodeEscapes(src) {
  return [...src.matchAll(/\\u\{([^}]*)\}/g)].every(([, hex]) =>
    /^[0-9a-fA-F]{1,6}$/.test(hex) && parseInt(hex, 16) <= 0x10FFFF
  );
}
// validate every \u{...} escape has 1-6 hex digits and value <= 10FFFF before parsing

Type guard

null

Try / catch

try {
  const result = parser.parse(source);
} catch (e) {
  if (e.errorId === 'invalidUnicodeEscapeSequence') {
    // correct the escape flagged in e.getMessage() at e.span
  } else { throw e; }
}

Prevention

When it happens

Trigger: Parsing a string containing a malformed `\u{...}` escape: non-hex characters inside the braces (`\u{12G4}`), nothing between the braces (`\u{}`), more than 6 hex digits (`\u{0010FFFF0}`), or a value beyond U+10FFFF such that Character.toString also fails.

Common situations: Copying `\uXXXX` escapes from JSON/Java (no braces) into Pkl; hand-typing escapes and mistyping a hex digit; generating escapes programmatically with a decimal code point instead of hex.

Related errors


AI-assisted analysis of apple/pkl@f3efcbfc9b (2026-09-08). Data as JSON: /api/errors/179dc10cd9dd7e22. Report an issue: GitHub.

Appendix: source

Thrown at pkl-parser/src/main/java/org/pkl/parser/ParserImpl.java:1664

      case STRING_ESCAPE_QUOTE -> "\"";
      case STRING_ESCAPE_BACKSLASH -> "\\";
      case STRING_ESCAPE_TAB -> "\t";
      case STRING_ESCAPE_RETURN -> "\r";
      case STRING_ESCAPE_CONTINUATION -> "";
      case STRING_ESCAPE_UNICODE -> parseUnicodeEscape(tk);
      default -> throw new RuntimeException("Unreacheable code");
    };
  }

  private String parseUnicodeEscape(FullToken tk) {
    var text = tk.text(lexer);
    var lastIndex = text.length() - 1;
    var startIndex = text.indexOf('{', 2);
    try {
      var codepoint = Integer.parseInt(text.substring(startIndex + 1, lastIndex), 16);
      return Character.toString(codepoint);
    } catch (NumberFormatException e) {
      throw new ParserError(
          ErrorMessages.create("invalidUnicodeEscapeSequence", text, text.substring(0, startIndex)),
          tk.span);
    }
  }

  private String getCommonIndent(List<TempNode> nodes, Span span) {
    var lastNode = nodes.get(nodes.size() - 1);
    if (lastNode.token == null) {
      throw new ParserError(
          ErrorMessages.create("closingStringDelimiterMustBeginOnNewLine"), lastNode.span());
    }
    if (lastNode.token.token == Token.STRING_NEWLINE) return "";
    var beforeLast = nodes.get(nodes.size() - 2);
    if (beforeLast.token != null && beforeLast.token.token == Token.STRING_NEWLINE) {
      var indent = getTrailingIndent(lastNode);
      if (indent != null) {
        return indent;
      }

View on GitHub (pinned to f3efcbfc9b)