Skip to content

Expose error location on exceptions and show the source line with a caret - #681

Merged
stencil-harness[bot] merged 2 commits into
masterfrom
robot/issue-314
Nov 11, 2026
Merged

stencil-harness[bot] merged 2 commits into
masterfrom
robot/issue-314

Conversation

@stencil-harness

Copy link
Copy Markdown
Contributor

Refs #314

Follow-up to #680, as requested on #314.

What's new

HandlebarsException.Line / HandlebarsException.Column are new public int? properties. They're inherited by HandlebarsCompilerException, HandlebarsParserException and HandlebarsRuntimeException.

  • Zero-based. They hold the same values as the existing Occured at: line:column text, so the two always agree.
  • null when the location isn't known. That includes exceptions created through the public constructors.

Source hint in the message. When a compile-time error has a location, the message now ends with the template line containing the error and a caret under the column:

Unexpected '!File' inside an expression: '!' can only start a comment (e.g. '{{! comment }}') and is not a negation operator. Use '#unless' to negate a condition

Occured at: 0:9

<p>{{#if !File}}yes{{/if}}</p>
         ^

Multi-line template, where the error is detected at the stray closing tag on line 3:

A closing element '/if' was found without a matching open element

Occured at: 3:4

  {{/if}}
    ^

How

  • ExtendedStringReader keeps the text it has read and where each line starts. That way the line is still available when an error is detected long after the reader passed it, e.g. an unclosed block reported at the end of the template. If the error's line hasn't been fully read yet, it reads ahead to finish the line, capped at 4096 characters.
  • HandlebarsCompiler.Compile and CompileView catch HandlebarsException, attach the hint, and rethrow with throw;, so the stack trace is unchanged. The hint goes onto the exception and onto any HandlebarsException it wraps. An exception that already has a hint, e.g. one from a nested layout, keeps the hint for its own source.
  • Message is overridden so that it, and ToString(), include the hint.
  • Formatting:
    • Only the physical line containing the error is shown, even if the template's line endings don't match Environment.NewLine.
    • Tabs before the column are kept so the caret lines up.
    • Lines longer than 120 characters are cut to a window around the column, with ... marking the cut.

Compatibility

  • No public API is removed or changed. The only additions are the two properties and the Message override. Exception types, constructors and the existing message text (including Occured at: l:c) are the same. The hint is only appended after them, and only for errors that already reported a location.
  • Callers that compare the full Message of a located compile error for exact equality will see the extra lines. Matching on type, prefix or Contains keeps working.
  • Compilation now keeps a copy of the template text until compilation finishes. Rendering is unaffected.

Tests

New tests in ExceptionTests cover:

  • the properties
  • the caret position
  • multi-line templates
  • \n and \r\n line endings
  • tabs
  • long-line truncation
  • a parser error
  • errors without a location

There's also a view test in ViewEngineTests. The full suite passes (2234 tests).

🤖 Generated with Claude Code

Closes #314

stencil-harness Bot and others added 2 commits November 10, 2026 20:08
…aret

HandlebarsException gains public Line and Column properties (zero-based,
same values as the existing "Occured at: line:column" text; null when
the location is unknown).

When a compile-time error has a location, the message now ends with the
template line containing the error and a caret under the column:

    Unexpected '!File' inside an expression: ...

    Occured at: 0:9

    <p>{{#if !File}}yes{{/if}}</p>
             ^

ExtendedStringReader keeps the text it has read, so the line is
available even when the error is detected after the reader has moved
on (e.g. an unclosed block reported at end of template). The hint is
attached in the compile entry points (Compile and CompileView), shows
only the physical line regardless of line-ending style, keeps tabs
aligned, and cuts very long lines to a window around the column.

Exception types, constructors and the existing message prefix are
unchanged.

Refs #314

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@stencil-harness
stencil-harness Bot merged commit dcbe4af into master Nov 11, 2026
7 checks passed
@stencil-harness
stencil-harness Bot deleted the robot/issue-314 branch November 11, 2026 01:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Please report betters errors (context) when having mistakes in the template

0 participants