Repository navigation
Description
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:
- Preserve the original exception's stack trace as the cause. Never throw away the underlying issue.
- Element ID path in the message. Uniquely identifies the position in the render tree; can be correlated with the rendered DOM.
- 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, theNGDynamicGrouprepetition) 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.
Activity