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.

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)

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.