Skip to content

Enrich exceptions thrown during rendering with positional context #61

@hugithordarson

Description

@hugithordarson

When an exception is thrown during the rendering of a dynamic element, the resulting stack trace shows the structural shape of the render but tells you almost nothing useful about where the error occurred.

The current pain

A simple formatting error in an NGTextField produces a stack trace like:

java.lang.IllegalArgumentException: Cannot format given Object as a Number
    at java.base/java.text.DecimalFormat.format(DecimalFormat.java:584)
    at ng.appserver.templating.elements.NGTextField.appendToResponse(NGTextField.java:117)
    at ng.appserver.templating.NGElement.appendOrTraverse(NGElement.java:23)
    at ng.appserver.templating.elements.NGDynamicGroup.appendChildrenToResponse(...)
    at ng.appserver.templating.elements.NGConditional.appendChildrenToResponse(...)
    [...many more framework frames, repeating patterns...]
    at ng.appserver.NGComponentRequestHandler.handleRequest(...)

From this you know: an NGTextField somewhere failed to format some number. You don't know:

  • Which page is being rendered
  • Which component file the text field lives in (a typical app has many)
  • The element ID path that identifies which text field
  • Which binding failed (value? numberformat?)
  • The actual value that failed to format
  • The component instance state at the time
  • Which row of which repetition, if any

Debugging means searching the codebase for plausible candidates and guessing.

The proposal

Wrap exceptions at the element-traversal boundary with positional context. Minimal sketch:

public abstract class NGElement {
    public final void appendOrTraverse(NGResponse response, NGContext context) {
        try {
            appendToResponse(response, context);
        } catch (NGRenderException e) {
            throw e; // Already wrapped — let it propagate
        } catch (Throwable t) {
            throw new NGRenderException(this, context, t);
        }
    }
    
    public abstract void appendToResponse(NGResponse response, NGContext context);
}

public class NGRenderException extends RuntimeException {
    public NGRenderException(NGElement element, NGContext context, Throwable cause) {
        super(buildMessage(element, context, cause), cause);
    }
    
    private static String buildMessage(NGElement element, NGContext context, Throwable cause) {
        return String.format(
            \"Render failed: %s at element ID %s in component %s%n  caused by: %s: %s\",
            element.getClass().getSimpleName(),
            context.elementID(),
            context.component() != null ? context.component().getClass().getSimpleName() : \"(unknown)\",
            cause.getClass().getName(),
            cause.getMessage()
        );
    }
}

About 30 lines of code. The error becomes:

NGRenderException: Render failed: NGTextField at element ID 3.2.1.0.4 in component MyPage
  caused by: java.lang.IllegalArgumentException: Cannot format given Object as a Number
    [original stack trace preserved as cause]

Three things are essential:

  1. Preserve the original exception's stack trace as the cause. Never throw away the underlying issue.
  2. Element ID path in the message. Uniquely identifies the position in the render tree; can be correlated with the rendered DOM.
  3. Component class (not just element type). "NGTextField in MyPage" is dramatically more actionable than "NGTextField."

The re-throw guard ("if already an NGRenderException, pass it through") ensures only the innermost element wraps. The outer frames don't re-wrap, so the rich context surfaces at the top of the trace rather than getting buried under N layers of "render failed: NGDynamicGroup at..."

Going further (optional)

A meaningful enhancement: include which binding failed. Requires per-element opt-in — each element wraps its binding evaluations:

// Inside NGTextField.appendToResponse
String formatted;
try {
    formatted = formatter.format(value);
} catch (Throwable t) {
    throw new NGBindingException(this, \"value\", value, t);
}

Error becomes:

NGRenderException: Render failed: NGTextField at element ID 3.2.1.0.4 in component MyPage
  caused by: NGBindingException: binding 'value' (resolved to \"hello world\") failed
    caused by: java.lang.IllegalArgumentException: Cannot format given Object as a Number

Worth doing for the most-used elements (NGString, NGTextField, NGRepetition, conditionals). Other elements can adopt incrementally.

Future directions

  • Source positions on elements — when templates are parsed, stamp each element with file + line. Error message includes MyPage.wo:42. Natural extension once template compilation (Compile templates to Java classes #60) lands; generated classes know their source positions.
  • Trace cleanup — filter framework-internal frames (appendOrTraverse, appendChildrenToResponse, the NGDynamicGroup repetition) from the displayed stack. Less destructive variant: annotate them so IDEs can collapse rather than removing entirely.
  • Cursor-based — once the render redesign lands and elements receive a cursor parameter, position info reads off the cursor in one place. The element-by-element wrapping above becomes a single top-level handler.
  • Inline error rendering — Parsley-style (in WO) renders the error visually at its position in the rendered page. Complementary to stack-trace enrichment; one is for logs, the other for development browsing. See related design conversations.

Scope for v1

The minimal version is genuinely an afternoon's work: add the wrap in appendOrTraverse, define NGRenderException. Per-binding wrapping in popular elements is an optional follow-up.

The cost is essentially zero (one catch block on the rendering hot path; if rendering succeeds the overhead is negligible) and the debug-time payoff is large.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions