apple/pkl · error · GenericParserError

closingStringDelimiterMustBeginOnNewLine

closingStringDelimiterMustBeginOnNewLine

Error message

The closing delimiter of a multi-line string must begin on a new line.

What it means

Pkl requires the closing `"""` of a multi-line string to start on its own line: the token immediately before the closer must be a STRING_NEWLINE. validateStringEndDelimiter inspects the second-to-last node; if it is not a newline and its text is not blank, the error is thrown at that node's span.

Solutions

  1. Move the closing `"""` onto its own new line.
  2. If a single-line string suffices, switch to `"..."` syntax.
  3. Fix templates/generators to always emit a newline before the closing delimiter.

Example fix

// before
msg = """
  hello"""

// after
msg = """
  hello
  """
Defensive patterns

Strategy: validation

Validate before calling

// Closing """ must be alone on its line (only indentation before it)
function closingDelimiterOnOwnLine(src) {
  return !/[^\n]"""\s*(\n|$)/.test(src.replace(/\\"/g, ''));
}

Prevention

When it happens

Trigger: Writing content and the closing `"""` on the same line, e.g. `"""\n hello"""` instead of `"""\n hello\n """`.

Common situations: Condensing multi-line strings to save lines; auto-formatters or editors joining lines; code generation emitting `value"""` on one line.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


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

Appendix: source

Thrown at pkl-parser/src/main/java/org/pkl/parser/GenericParserImpl.java:1049

        }
        case EOF -> {
          var delimiter = new StringBuilder(start.text(lexer)).reverse().toString();
          throw parserError("missingDelimiter", delimiter);
        }
      }
    }
    children.add(makeTerminal(next())); // string end
    validateStringEndDelimiter(children);
    validateStringIndentation(children);
    return new Node(NodeType.MULTI_LINE_STRING_LITERAL_EXPR, children);
  }

  private void validateStringEndDelimiter(List<Node> nodes) {
    var beforeLast = nodes.get(nodes.size() - 2);
    if (beforeLast.type == NodeType.STRING_NEWLINE) return;
    var text = beforeLast.text(lexer.getSource());
    if (!text.isBlank()) {
      throw parserError(
          ErrorMessages.create("closingStringDelimiterMustBeginOnNewLine"), beforeLast.span);
    }
  }

  private void validateStringIndentation(List<Node> nodes) {
    var indentNode = nodes.get(nodes.size() - 2);
    if (indentNode.type == NodeType.STRING_NEWLINE) return;
    var indent = indentNode.text(lexer.getSource());
    var previousNewline = false;
    for (var i = 1; i < nodes.size() - 2; i++) {
      var child = nodes.get(i);
      if (child.type != NodeType.STRING_NEWLINE && previousNewline) {
        var text = child.text(lexer.getSource());
        if (!text.startsWith(indent)) {
          throw parserError(ErrorMessages.create("stringIndentationMustMatchLastLine"), child.span);
        }
      }
      previousNewline =

View on GitHub (pinned to f3efcbfc9b)