apple/pkl · error · ParserError
closingStringDelimiterMustBeginOnNewLine
closingStringDelimiterMustBeginOnNewLine
Error message
The closing delimiter of a multi-line string must begin on a new line.
What it means
In a Pkl multi-line string, the closing `"""` delimiter must sit on its own line. getCommonIndent inspects the last node of the string's content; if that node is a raw text part with no token boundary (meaning the closer shares a line with content, leaving no trailing-newline token), it throws closingStringDelimiterMustBeginOnNewLine at the last node's span.
Source
Thrown at pkl-parser/src/main/java/org/pkl/parser/ParserImpl.java:1673
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;
}
}
throw new ParserError(ErrorMessages.create("closingStringDelimiterMustBeginOnNewLine"), span);
}
private @Nullable String getTrailingIndent(TempNode node) {
var token = node.token;
if (token == null || token.token != Token.STRING_PART) return null;
var text = token.text(lexer);
for (var i = 0; i < text.length(); i++) {View on GitHub (pinned to f3efcbfc9b)
Solutions
- Place the closing `"""` on its own new line after the last content line
- Disable trimming of the final newline if a build/template step strips it before parsing
- Reformat the string block so a line break precedes the closing delimiter
Example fix
// before msg: """ hello""" // after msg: """ hello """
Defensive patterns
Strategy: validation
Validate before calling
function closingDelimiterOnOwnLine(src) {
return [...src.matchAll(/"""/g)].length % 2 === 0 &&
!/[^\n]"""\s*$|[^\n]"""/m.test(src.split('"""')[1] ?? '');
}
// simpler: for each multi-line string, assert the closing '"""' is the first non-space token on its line Type guard
null
Try / catch
try {
const result = parser.parse(source);
} catch (e) {
if (e.errorId === 'closingStringDelimiterMustBeginOnNewLine') {
// insert a newline before the closing triple quote at e.span
} else { throw e; }
} Prevention
- Always end multi-line string content with a newline before the closing """
- Turn off editor/template trimming that removes trailing newlines
- Adopt the standard indented """ block style in your codebase
When it happens
Trigger: Parsing a multi-line string whose closing delimiter appears on the same line as text, e.g. `""" hello"""` — the last content node lacks a token so the parser cannot compute the string's common indent.
Common situations: Writing Kotlin/Scala-style raw strings where the closer trails content; collapsing the final newline during copy-paste or minification; template engines trimming the trailing newline before the closer.
Related errors
- stringContentMustBeginOnNewLine
- missingDelimiter
- unexpectedEndOfFile
- stringIndentationMustMatchLastLine
- notAUnion
AI-assisted analysis of apple/pkl@f3efcbfc9b (2026-09-08).
Data as JSON: /api/errors/680318ad4a4d6388.
Report an issue: GitHub.