Error catalog
Resolve every parse and compile diagnostic SML emits with the source condition, an example, and the fix.
Every SML diagnostic carries a source span and a message that names the fix, the
shape is line L:C <message>, e.g.:
line 12:8 unknown class "flex", display:flex is unsupported in Outlook for Windows (Word engine), use <Row>/<Column>Parse errors fail fast (the first one aborts parsing); class-resolution errors are collected so you can fix a whole attribute's worth of classes at once. This page groups every diagnostic by the source condition that triggers it.
Lexer errors
Raised while tokenizing, before the parser sees a well-formed tag or text run.
| Condition | Example | Fix |
|---|---|---|
| Unterminated attribute value | href="https://example.com (missing closing quote) | Add the closing quote. |
Bare & in text or an attribute value | Ben & Jerry's | Escape it: Ben & Jerry's. |
| Malformed interpolation | {{ first name }}, {{}} | Use letters, digits, underscore, or dot only: {{firstName}}. |
| HTML comment or declaration | <!-- note -->, <!DOCTYPE html> | SML has no comment or doctype syntax, remove it. |
Stray < in text | 5 < 10 | Escape it: 5 < 10. |
| Unexpected character inside a tag | A stray symbol between attributes | Remove it; only identifiers, =, quotes, {/}, /, > are valid inside a tag. |
Unexpected / inside a tag | <Image / > with content following | Use /> immediately to self-close, with nothing after the tag name and attributes. |
Tag and structure errors
Raised while parsing tags, close tags, and the document shape.
| Condition | Example | Fix |
|---|---|---|
| Unknown tag | <Sectoin> | Typo suggestions are offered when close (did you mean <Section>?); otherwise the full tag list is listed. |
| Unknown attribute | <Text color="red"> | <Text> only accepts class, check the primitive reference. |
| Duplicate attribute | <Image src="a.png" src="b.png" /> | Remove one. |
| Missing required attribute | <Button>Click</Button> (no href) | Add the required attribute, e.g. href="...". |
| Attribute value not quoted | width=50% | Quote strings: width="50%"; wrap numbers in braces: height={24}. |
| Attribute missing a value entirely | <Spacer height> | Add = and a value: height={24}. |
| Number attribute given a string | level="2" | Use braces for numbers: level={2}. |
| String attribute given braces | href={docs} | Quote it: href="docs". |
| Number out of bounds | <Heading level={9}>, <Spacer height={5000} /> | Stay within the attribute's documented range (see the primitive reference). |
| Void element not self-closed | <Spacer height={24}></Spacer> | Self-close it: <Spacer height={24} />. |
| Unclosed tag | <Text>Hello with no </Text> | Add the matching closing tag. |
| Mismatched closing tag | <Text>...</Heading> | Close the innermost open tag first. |
Text or {{variable}} directly inside a block element | <Section>{{issueNumber}}</Section> | Wrap it in <Text>: <Section><Text>{{issueNumber}}</Text></Section>. |
| Child not allowed in this content model | <a> nested inside <Button> or another <a> | Links cannot nest inside links or buttons, restructure the content. |
<Preview> not first | A block before <Preview> inside <Email> | Move <Preview> to be the very first child of <Email>. |
Document doesn't start with <Email> | A <Container> (or any tag) as the root | Wrap everything in a single <Email> root. |
Root element isn't <Email> | <Container>...</Container> at the top level | The document root must be <Email>. |
Content after </Email> | Trailing text or a second element after the closing </Email> | A document has exactly one <Email> root, remove anything after it. |
<Else> not last inside <If> (else-not-last) | A block after <Else> inside <If> | Move <Else> to be the last child of <If>. |
More than one <Else> inside <If> (duplicate-else) | Two <Else> blocks inside the same <If> | Only one <Else> is allowed, merge the branches. |
Predicate errors
Raised while parsing an <If test="..."> predicate, see
Conditionals for the full grammar.
Predicate spans are offset into the containing document, so they point at the
exact character inside the test="..." attribute.
| Condition | Example | Fix |
|---|---|---|
predicate-empty | test="" | Write a predicate, e.g. test="plan == 'pro'". |
predicate-path-not-supported | test="user.plan == 'pro'", test="plan.toUpperCase() == 'PRO'" | Predicates use flat variable names only, no paths, no method calls. Pass a derived flat variable instead. |
predicate-unexpected-character | test='plan == "pro"' (double-quoted string), test="seats + 1 > 5" (arithmetic) | Use single-quoted strings and drop unsupported characters/operators, the grammar has no arithmetic. |
predicate-invalid-operator | test="plan = 'pro'" (single =), test="plan ! 'pro'" | Use ==/!=/>/>=/</<=. |
predicate-unterminated-string | test="plan == 'pro" (missing closing quote) | Add the closing '. |
predicate-string-double-dash | test="note == 'a--b'" | String literals can't contain --, predicates are embedded verbatim in HTML comment markers, and -- would end the comment early. |
predicate-expected-literal | test="seats >", test="plan ==" (operator with no right-hand side) | Add a literal after the operator: seats > 5 or plan == 'value'. |
predicate-unexpected-token | test="'pro' == plan" (literal first), test="not and" (keyword as name) | Comparisons put the variable first: name == 'value'. Reserved words (and/or/not) can't be used as variable names. |
predicate-unexpected-end | test="not", test="plan and", test="(" (predicate cut off mid-expression) | Finish the expression, a variable name, comparison, or ( group ) is expected. |
predicate-missing-paren | test="(a or b" (unclosed group) | Add the closing ). |
predicate-trailing-input | test="a b", test="a == 'x' == 'y'" (chained comparison) | Join clauses with and/or, comparisons don't chain. |
Class-resolution errors
Raised per class while resolving a class attribute, see the
class-subset reference for the full supported list.
| Condition | Example | Fix |
|---|---|---|
| Unknown class | class="flex-col" (not in the registry) | Use a supported utility, the error lists the supported families, or the valid values for that root. |
| Forbidden utility | class="flex", class="shadow-lg" | Named per class in the class-subset reference, each names the failing client and the primitive/utility to use instead. |
| Unsupported variant | class="md:text-lg", class="hover:underline" | Only sm: and dark: are supported. md:/lg:/xl:/2xl: → use sm:. hover:/focus:/active: → remove it, email clients don't apply pseudo-state styles. |
| More than one variant on a class | class="sm:dark:bg-blue-400" | At most one variant per class, split into separate sm: and dark: classes. |
dark: on a non-color utility | class="dark:p-4", class="dark:hidden" | dark: applies only to color utilities (bg-*, text-{color}, border-{color}), remove dark: or restyle with a color family. See variants. |
Trailing !important modifier | class="text-lg!" | Remove the trailing !, the compiler manages !important for sm:/dark: rules itself. |
| Opacity/alpha modifier | class="bg-black/50" | Remove the /modifier, it compiles to an alpha color, which renders solid black in Outlook for Windows. Use a solid color token instead. |
| Rejected arbitrary function | class="bg-[var(--brand)]", class="p-[calc(1rem+2px)]", class="text-[oklch(0.6_0.1_250)]", class="bg-[rgb(0,0,0,0.5)]", class="bg-[hsl(210,50%,50%)]" | Compiled email has no CSS custom properties and no reliable alpha/functional-color support, use a literal hex color or px value. |
| Warn-tier class (compiles, but flagged) | class="mx-4", class="mt-2" | Margin compiles but is stripped by some clients (Outlook.com dark mode, some webmail), prefer padding on the parent or <Spacer> for vertical gaps. |
Internal errors
| Condition | Meaning |
|---|---|
internal-unexpected-node | An inline tag (strong/em/a) reached block expansion, the parser's content model should make this unreachable. If you see this, it's a compiler bug, not a source problem, please report it. |
Debugging tips
- Fix diagnostics top-to-bottom in a file, later errors sometimes cascade from an earlier unclosed tag or misplaced element.
- Class-resolution errors are collected per attribute, so a
classwith three unsupported utilities reports all three at once rather than one per retry. - The class-subset reference and primitive reference are generated from and validated against the same registries the compiler resolves against, so "is this class/attribute supported" is always answerable from the docs without trial and error.