en Error Handling - chiba233/yumeDSL GitHub Wiki

Error Handling

Source Position Tracking | Vue 3 Rendering

The parser never throws. Broken tags? Degraded to plain text, output continues. The public error callback belongs to the render-facing APIs: parseRichText / stripRichText. parseStructural intentionally does not expose error semantics on its public surface.


How errors flow

DSL source text
    │
    ▼
Parser scans
    │
    ├─ Tag OK → TextToken (normal output)
    │
    └─ Tag broken → degrade to plain text
                    │
                    ├─ onError provided? → call it with ParseError
                    └─ not provided?     → silently discard, keep parsing

Core principle: no matter how bad the input, never crash — just degrade.


onError Callback

onError is a public capability of the render pipeline APIs, used to collect final error semantics:

const errors: ParseError[] = [];
parseRichText("$$bold(unclosed", {
    onError: (error) => errors.push(error),
});
// errors[0]:
// {
//   code: "INLINE_NOT_CLOSED",
//   message: "(L1:C1) Inline tag not closed:  >>>$$bold(<<< unclosed",
//   line: 1,
//   column: 1,
//   snippet: " >>>$$bold(<<< unclosed"
// }

If onError omitted: errors silently discarded. Parser never throws.

stripRichText("$$bold(unclosed", {
    onError: (error) => console.log(error.code),
});
// INLINE_NOT_CLOSED

Why parseStructural has no public onError

parseStructural is the structure view, not the final error-semantics API.

  • parseStructural answers "what raw structure is present in the source?"
  • parseRichText / stripRichText answer "how should this be interpreted, degraded, and reported?"

So parseStructural intentionally keeps onError out of StructuralParseOptions. There is still an internal scan-time error channel for upper-layer APIs to reuse, but that is an implementation detail, not public contract.


ParseError Interface

interface ParseError {
    code: ErrorCode;
    message: string;
    line: number;
    column: number;
    snippet: string;
}
Field What it is
code Error code (ErrorCode union type). Use code for programmatic branching — exhaustive switch possible
message Human-readable error description with (L1:C1) prefix and >>>...<<< markers. Intended for display, not parsing — the format may change between versions
line / column 1-indexed. When trackPositions is active, reuses the same line-offset table
snippet Context around the error, >>> <<< mark the problematic span

Error Codes

type ErrorCode =
    | "DEPTH_LIMIT"
    | "UNEXPECTED_CLOSE"
    | "INLINE_NOT_CLOSED"
    | "SHORTHAND_NOT_CLOSED"
    | "BLOCK_NOT_CLOSED"
    | "BLOCK_CLOSE_MALFORMED"
    | "RAW_NOT_CLOSED"
    | "RAW_CLOSE_MALFORMED";
Code What happened Example trigger
DEPTH_LIMIT Nesting too deep, exceeded depthLimit 51 levels of $$a($$b(...
UNEXPECTED_CLOSE Stray close tag with no matching open. Suppressed when )$$ is immediately followed by a valid tag head (since 1.3.7) lone )$$
INLINE_NOT_CLOSED Inline tag opened but never closed $$bold(unclosed
SHORTHAND_NOT_CLOSED Implicit inline shorthand opened but never closed (since 1.3) bold(unclosed with implicitInlineShorthand enabled
BLOCK_NOT_CLOSED Block close marker missing $$info()* content but no *end$$
BLOCK_CLOSE_MALFORMED Block close exists but wrong format *end$$ not on its own line
RAW_NOT_CLOSED Raw close marker missing $$code()% content but no %end$$
RAW_CLOSE_MALFORMED Raw close exists but wrong format %end$$ not on its own line

Graceful Degradation

The parser NEVER throws on malformed input. Different scenarios degrade differently:

Unregistered tags

Degradation depends on the tag form:

  • Inline $$unknown(hi)$$ → outputs only the inner content hi (delimiters stripped, content kept as plain text)
  • Raw/Block $$unknown()%\nstuff\n%end$$ → entire raw markup output as literal text (parser doesn't recognize the tag at all, so all delimiters are preserved)
// inline: content unwrapped
dsl.parse("$$unknown(hello)$$");
// → [{ type: "text", value: "hello" }]

// raw/block: entire markup preserved
dsl.parse("$$unknown()%\nstuff\n%end$$");
// → [{ type: "text", value: "$$unknown()%\nstuff\n%end$$" }]

Unsupported form

When a handler exists but doesn't support the form the user wrote (e.g., handler only has inline, user writes raw form), the entire raw markup is output as literal text:

// code handler only has raw, user writes inline
dsl.parse("$$code(ts)$$");
// → [{ type: "text", value: "$$code(ts)$$" }]

allowForms restriction

Forms disabled by allowForms are treated as unsupported — same behavior as unsupported form, entire raw markup output as literal text.

Unclosed tags

Parser reports error (via onError), opening markup recovered as plain text. No exception thrown.


Error-semantics cleanup after 1.1.1

4 false-positive errors removed from parseRichText

parseRichText previously emitted a spurious INLINE_NOT_CLOSED when encountering complex-form tags (raw/block syntax) in certain degradation scenarios. These 4 false positives are removed — the parser no longer reports an error for tags that are correctly degrading through the complex-form path.

By 1.1.3 and the current version, the complex-form error codes are stable. For example:

  • $$raw-code(js)%\nconst x = 1RAW_NOT_CLOSED
  • $$info(T)*\nhelloBLOCK_NOT_CLOSED
  • $$raw-code(ts)%\nconst x = 1\n%end$$ trailingRAW_CLOSE_MALFORMED
  • $$info()*\nhello\n*end$$ trailingBLOCK_CLOSE_MALFORMED