skip to content

In a module-based Angular app, why does a template fail with 'app-user-card is not a known element', and how does NgModule compilation scope explain the fix?

level: middleimportance: must knowfreq 68%

answer

  1. scope of the declaring module
  2. own declarations plus imported exports
  3. exports are not transitive
  4. NG8001 at compile time

basics

~20 s

A declared component's template can only use its own module's declarations plus what imported modules export. The error means UserCard is not in that scope: export it from its module and import that module into the one declaring the failing template.

solid answer

~30 s

Each component declared in an NgModule is compiled against that module's compilation scope: the module's own declarations, the exports of every module it imports, and standalone classes it imports directly. `'app-user-card' is not a known element` (`NG8001` from the AOT compiler) means `UserCard` is outside that scope. Fix it by exporting `UserCard` from the module that declares it and importing that module into the module declaring the failing template. Exports are not transitive unless re-exported, and what the root module imports does not leak into feature modules. `CUSTOM_ELEMENTS_SCHEMA` is only for real web components, not a way to silence the error.

code

ts · 15 lines
ts
import { NgModule } from '@angular/core';
import { UserCard } from './users/user-card';
import { OrderDetail } from './orders/order-detail'; // template uses <app-user-card>

@NgModule({
  declarations: [UserCard],
  exports: [UserCard], // without this, importers cannot see <app-user-card>
})
export class UsersModule {}

@NgModule({
  declarations: [OrderDetail],
  imports: [UsersModule], // brings UsersModule's exports into OrderDetail's scope
})
export class OrdersModule {}

go deeper

for a junior

Know the fix: the component must be exported by its own module, and that module imported by the module that declares the template using it.

for a middle

Define compilation scope precisely: own declarations, imported modules' exports, directly imported standalone classes, and explain why exports are not transitive.

for a senior

Trace a scope error across several modules quickly, and push back on CUSTOM_ELEMENTS_SCHEMA or duplicate declarations used to silence it.

for a principal

Use recurring scope errors as evidence when arguing for standalone migration, since standalone puts each template's dependencies in the same file.

## The error In a module-based Angular app, a template that uses a component the compiler cannot resolve fails to build: ``` NG8001: 'app-user-card' is not a known element: 1. If 'app-user-card' is an Angular component, then verify that it is part of this module. 2. If 'app-user-card' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' ... ``` `NG8001` is the ahead-of-time compiler's error; the runtime equivalent in JIT-compiled code is `NG0304`. A similar message for an unknown property binding appears as `NG8002`. All of them come from the same rule: **compilation scope**. ## What compilation scope is Every component declared in an NgModule is compiled against that module's **compilation scope** — the set of selectors and pipe names its template is allowed to use. For a component declared in module `M`, the scope is: 1. Everything **declared** in `M` itself. 2. Everything **exported** by each module listed in `M`'s `imports`. 3. Every **standalone** component, directive or pipe listed directly in `M`'s `imports`. That is the whole list. In particular: - A module's **non-exported** declarations are private. Importing the module does not reveal them. - Exports are **not transitive by default**. If `M` imports `A`, and `A` imports `B`, `M` does not see `B`'s exports unless `A` also lists `B` (or the specific class) in its own `exports`. - Scope is about the **declaring** module, not the module where the component happens to be rendered. A component declared in `OrdersModule` is compiled against `OrdersModule`'s scope, even when it appears inside a page that belongs to another module. ## Diagnosing `'app-user-card' is not a known element` Work through the chain from the failing template back to the component: 1. **Find the declaring module of the failing template.** Which module declares the component whose template uses `<app-user-card>`? That module's scope is the one that matters. 2. **Find where `UserCard` is declared.** Say it is in `UsersModule`. 3. **Check `UsersModule`'s `exports`.** If `UserCard` is not exported, no importer can use it. Add it to `exports`. 4. **Check the failing module's `imports`.** It must import `UsersModule` (or a module that re-exports it). 5. **Check the selector.** A typo or a changed selector gives the same error. 6. **Check it is not a web component.** For genuine custom elements, `CUSTOM_ELEMENTS_SCHEMA` tells the compiler to accept unknown tags — it should not be used to silence a missing import. ## Why this confuses people | Belief | Reality | |---|---| | "The parent page imports `UsersModule`, so the child can use it" | Only the child's **own declaring module** counts | | "`AppModule` imports it, so it is available everywhere" | Scope is per module; importing in the root module does not leak into feature modules | | "It is exported by a module my module's import imports" | Exports are not transitive unless re-exported | | "Adding it to `declarations` again fixes it" | A second declaration is itself an error | The same logic explains the property error `Can't bind to 'user' since it isn't a known property of 'app-user-card'` — usually either the component is not in scope (so the tag is treated as a plain element) or the input name is wrong. ## Scope and dependency injection are separate Compilation scope only concerns **templates**. Whether a service can be injected depends on **providers** and the injector tree, not on imports and exports. A component can be in scope and still fail to inject a service, and vice versa. Keeping those two mental models apart is most of what interviewers probe here. ## Why standalone mostly removes the problem A standalone component has no declaring module: its template scope is exactly the classes listed in its own `imports`. The error can still happen — you forgot to import something — but the fix lives in the same file as the template, rather than two or three modules away. That locality is one of the strongest arguments for standalone-first code. The compiler even words the error differently for the two cases. For a component declared in a module, its first hint says to verify the element "is part of this module"; for a standalone component, it says to verify it is "included in the '@Component.imports' of this component". Reading which variant you got tells you immediately whether you are debugging module scope or a single component's `imports` list.

  • OrdersModule imports SharedModule, which imports UsersModule. Why is app-user-card still unknown in OrdersModule?
    Exports are not transitive. SharedModule's own templates can use `UsersModule`'s exports, but `OrdersModule` sees only what `SharedModule` itself exports. Add `UsersModule`, or `UserCard`, to `SharedModule`'s `exports` so it is re-exported, or import `UsersModule` directly into `OrdersModule`.
  • When is CUSTOM_ELEMENTS_SCHEMA the right fix for this error?
    Only when the tag is a genuine custom element defined outside Angular, such as a web component registered with the browser. Adding it for an Angular component hides the real problem: the component is never instantiated, the tag renders as an inert element, and input bindings silently stop working.
  • Does moving the import into AppModule make the component available to every feature module?
    No. Compilation scope is computed per declaring module. Importing `UsersModule` into `AppModule` only affects components declared in `AppModule`; each feature module must import what its own templates use.

saying these in an interview costs you the question

  • Importing a module into AppModule makes its components usable app-wide
  • A child component can use whatever the parent page's module imports
  • Importing a module gives access to its non-exported declarations
  • Modules re-export their imports automatically
  • CUSTOM_ELEMENTS_SCHEMA is the standard fix for 'not a known element'