Salesforce Dictionary - Free Salesforce GlossarySalesforce Dictionary
All errors
Lightning · LWC

LWC1009: <template> directives are not allowed at the top level

You put `if:true`, `for:each`, `iterator`, or another LWC template directive on the *root* `<template>` of an LWC HTML file. Directives belong on inner `<template>` tags or on regular elements, not on the outermost wrapper.

Also seen asLWC1009·template directives are not allowed at the top level·LWC1009: <template> directives·if:true template top level

The save fails before the component ever ships. The Salesforce CLI returns LWC1009: <template> directives are not allowed at the top level, and you spend the next five minutes squinting at HTML that looks fine to you because you cut and pasted it from a working LWC tutorial five minutes ago.

What the compiler is checking

Every Lightning Web Component HTML file has a single outermost element, and it is always <template>. That outermost template is a render barrier, not a directive carrier. The compiler refuses to let you put if:true, if:false, for:each, iterator:, lwc:if, lwc:elseif, lwc:else, lwc:slot-data, or any other template directive on it.

The error code LWC1009 is the compiler's way of saying "this directive lives on an inner template or an element, never on the root."

The reason is architectural. The root <template> represents the component itself. Whether the component appears in the rendered page is a decision the parent makes (by mounting or unmounting <c-my-thing>), not a decision the component can make about itself. Allowing a directive on the root would create an ambiguity: does if:false mean "don't render anything" or "don't mount the component"? The compiler closes the question by rejecting the syntax.

The broken example

The shortest path to LWC1009:

<!-- accountSummary.html: LWC1009 -->
<template if:true={hasAccount}>
  <p>Account is {account.Name}</p>
  <p>Tier: {account.Tier__c}</p>
</template>

Variations the compiler also rejects:

<!-- Rejected: for:each on the root -->
<template for:each={accounts} for:item="acct">
  <p>{acct.Name}</p>
</template>

<!-- Rejected: lwc:if on the root -->
<template lwc:if={loaded}>
  <p>Loaded.</p>
</template>

<!-- Rejected: lwc:slot-data on the root -->
<template lwc:slot-data="info">
  <p>{info.label}</p>
</template>

All three fail with the same compiler diagnostic. None of them deploy.

The fix

Move the directive onto an inner template that wraps the conditional or iterated content. The root stays bare.

<!-- accountSummary.html: corrected -->
<template>
  <template lwc:if={hasAccount}>
    <p>Account is {account.Name}</p>
    <p>Tier: {account.Tier__c}</p>
  </template>
</template>

For lists:

<template>
  <ul>
    <template for:each={accounts} for:item="acct">
      <li key={acct.Id}>{acct.Name}</li>
    </template>
  </ul>
</template>

For if/elseif/else (this requires API version 59.0 or higher):

<template>
  <template lwc:if={loaded}>
    <p>Account is {account.Name}</p>
  </template>
  <template lwc:elseif={error}>
    <p class="error">{error.message}</p>
  </template>
  <template lwc:else>
    <p>Loading...</p>
  </template>
</template>

The fixed example

Take the broken accountSummary and rewrite it as the complete corrected component:

<!-- accountSummary.html -->
<template>
  <lightning-card title="Account Summary" icon-name="standard:account">
    <template lwc:if={loading}>
      <lightning-spinner alternative-text="Loading"></lightning-spinner>
    </template>
    <template lwc:elseif={error}>
      <p class="slds-text-color_error">Could not load account: {error.message}</p>
    </template>
    <template lwc:else>
      <div class="slds-p-around_medium">
        <p><strong>Name:</strong> {account.Name}</p>
        <p><strong>Industry:</strong> {account.Industry}</p>
        <p><strong>Annual Revenue:</strong> {account.AnnualRevenue}</p>
      </div>
    </template>
  </lightning-card>
</template>
// accountSummary.js
import { LightningElement, api, wire } from 'lwc';
import { getRecord } from 'lightning/uiRecordApi';
import NAME_FIELD from '@salesforce/schema/Account.Name';
import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';
import REVENUE_FIELD from '@salesforce/schema/Account.AnnualRevenue';

const FIELDS = [NAME_FIELD, INDUSTRY_FIELD, REVENUE_FIELD];

export default class AccountSummary extends LightningElement {
    @api recordId;
    account;
    error;
    loading = true;

    @wire(getRecord, { recordId: '$recordId', fields: FIELDS })
    wiredAccount({ data, error }) {
        this.loading = false;
        if (data) {
            this.account = {
                Name: data.fields.Name.value,
                Industry: data.fields.Industry.value,
                AnnualRevenue: data.fields.AnnualRevenue.value,
            };
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.account = undefined;
        }
    }
}

The root template is bare. Three inner templates handle the loading, error, and success states. The compiler accepts the file. The component renders correctly in all three states.

When the parent should do the conditional rendering

If the entire component should sometimes not render at all, the conditional belongs in the parent's template, not in the child's root.

<!-- dashboard.html: the parent decides whether to mount accountSummary -->
<template>
  <template lwc:if={showAccountPanel}>
    <c-account-summary record-id={selectedAccountId}></c-account-summary>
  </template>
</template>

This is a clean separation. The child component is unaware of the conditional and always renders meaningfully when it exists. The parent owns the decision to mount or unmount.

The performance distinction matters. Mounting and unmounting via lwc:if in the parent fully tears down and recreates the child component, including its lifecycle hooks (connectedCallback, disconnectedCallback). Hiding the child via CSS (display: none) keeps it alive in the DOM but invisible, preserving state across hides. Choose based on what you want.

The modern syntax vs the older syntax

Two directive families coexist for backward compatibility:

OlderModern (API 59.0+)Notes
if:true={expr}lwc:if={expr}Render when expr is truthy
if:false={expr}(use lwc:if={!expr} or lwc:else)Render when expr is falsy
(no equivalent)lwc:elseif={expr}Chain conditions cleanly
(no equivalent)lwc:elseDefault branch

The modern family supports elseif and else, which the older family lacked. Two if-false blocks were the workaround in older code, and they made for noisy templates. New components should use lwc:if / lwc:elseif / lwc:else. Existing components don't have to migrate, but mixing the two families in the same component is a LWC1013 error, which is its own pain.

Either family triggers LWC1009 if you put them on the root template. The rule is the directive's location, not its name.

Other LWC1xxx errors that look similar

The LWC compiler emits a numbered diagnostic for every syntactic refusal. The ones that read the most like LWC1009:

CodeCause
LWC1009A template directive sits on the root <template>
LWC1011A for:each element is missing a key attribute
LWC1012Two key values in a for:each are duplicate at render time
LWC1013Older if:true mixed with modern lwc:if in the same template
LWC1014An unknown directive name was used (typo)
LWC1043A directive's expression is not a valid binding

Each diagnostic has a stable code that the LWC docs catalog, with a short example. When you hit one, the official compiler errors page is the fastest way to confirm the fix.

Why this rule exists at all

A short answer: predictability of component identity.

A longer answer: the framework needs a stable handle on each component instance to manage lifecycle, slots, parent-child wiring, and reactive bindings. If the root template were itself conditional, the runtime would have two concepts of "does this component exist" (the parent mounted it, and the child's root said yes), and reconciling them at every render would create both a performance cost and a class of bugs. Refusing the syntax up front sidesteps the problem.

The trade-off is small. You write one extra inner template per conditional or iteration. The framework gets a clean contract. The error message catches the mistake at compile time, before deploy, so you never ship a broken component.

Migration from Aura

In Aura components, the root <aura:component> carries attributes, controllers, handlers, and a body. Aura developers reaching LWC instinctively reach for if:true on the root because that's where conditional rendering belonged in Aura's aura:if. The new framework moves the directive inward, and the compiler error is the gentle nudge that the model has changed.

If you're porting an Aura aura:if from the root, the LWC equivalent is lwc:if on an inner template inside the LWC root. The semantic is the same; only the placement moved.

Other Aura habits that don't translate one-for-one:

  • aura:iteration becomes for:each (or its newer cousin), and the iterated element must have a key.
  • aura:set becomes a JavaScript property assignment, not a markup attribute.
  • aura:method becomes a public method decorated with @api.

The Aura-to-LWC migration guide in Salesforce's official docs catalogs the rest of these shifts. Hitting LWC1009 during a port is a normal step in the migration, not a sign that anything is broken.

What about lwc:dom and lwc:external

A handful of directives are component-level attributes that do live on the root, but they aren't conditional or iterative. The two main ones:

  • lwc:render-mode="light" declares the component as a Light DOM component instead of the default Shadow DOM. It is set on the root template.
  • lwc:external is used inside a parent component's slot consumption; it never appears on the root of the component that owns it.

The distinction is that these are metadata about the component itself, not directives that gate the rendering of its content. The compiler welcomes them on the root. LWC1009 only fires for the rendering-gate directives (if:true, if:false, lwc:if, lwc:elseif, lwc:else, for:each, iterator:, lwc:slot-data).

The mental model: directives that say "render this conditionally" or "render this multiple times" are content-level concerns and need an inner template. Directives that say "the entire component behaves this way" are component-level and can sit on the root.

A common variant: empty root template wrapping a directive

Some developers, on hitting LWC1009 for the first time, try this workaround:

<!-- Still rejected in some compilers -->
<template>
  <template if:true={hasContent} for:each={items} for:item="item">
    <p key={item.id}>{item.label}</p>
  </template>
</template>

Two directives on the same inner template is its own diagnostic (the compiler refuses to combine if:true and for:each on one element because the execution order is ambiguous). Split them:

<template>
  <template lwc:if={hasContent}>
    <template for:each={items} for:item="item">
      <p key={item.id}>{item.label}</p>
    </template>
  </template>
</template>

Outer template gates the rendering; inner template iterates. Each directive has one job.

Debugging when the error message points at the wrong line

LWC compiler errors carry a line number, but a misnamed file or an HTML element with mismatched tags can shift the apparent location. If LWC1009 complains about a line that looks innocent:

  1. Validate the HTML parses cleanly. An unclosed <div> higher up will push the parser into a confused state where the next template directive is misread as being on the root.
  2. Check the file's first line. Some editors insert a BOM or a stray whitespace character that makes the compiler treat the first real <template> as a nested one.
  3. Run the LWC linter (eslint-plugin-lwc) locally. The lint surfaces the same problems with friendlier messages, and it runs faster than a full deploy.

The fastest deploy-cycle debugger is sf project deploy preview --target-org X --source-dir force-app/main/default/lwc/myComponent. It validates locally and shows the error without hitting the org, saving a minute per iteration.

Performance and accessibility notes that ride along with the fix

Once you've moved the directive off the root, two small bonuses come free.

First, accessibility. An inner template wrapping conditional content gives you a clear surface on which to attach aria-live, aria-busy, or role="status" attributes when the content represents loading or error states. The root template has no rendered output, so it can't carry these attributes. Inner templates render real DOM and can.

Second, performance. The framework's reconciliation algorithm walks the tree from the root, applying directives at each level. A directive on an inner template short-circuits the walk for the gated subtree when the condition is false. The compiler refuses the root placement partly because it would require special-casing the top-level walk in every render, slowing down every component for the benefit of a rare pattern. Moving the directive inward keeps the hot path fast.

These are second-order benefits, not reasons to design around LWC1009. The reason to fix the error is that the compiler won't accept the file. The reason the rule exists is the architectural one. The reason you'll appreciate the rule a year later is the accessibility and performance hooks it gives you for free.

Further reading from Salesforce

Related dictionary terms

Share this fix

Share on LinkedInShare on X

Related Lightning · LWC errors