angular/angular-cli · error
Builder is not a builder
Error message
Builder is not a builder
What it means
loadBuilder imports a builder module and verifies it carries the internal BuilderSymbol marking a properly exported Angular builder (via createBuilder). If the imported module (or its default export) lacks this symbol, Architect rejects it with 'Builder is not a builder'.
Source
Thrown at packages/angular_devkit/architect/node/node-modules-architect-host.ts:281
const projectName = typeof target === 'string' ? target : target.project;
const metadata = this.workspaceHost.getMetadata(projectName);
return metadata;
}
async loadBuilder(info: NodeModulesBuilderInfo): Promise<Builder> {
const builder = await getBuilder(info.import);
if (builder[BuilderSymbol]) {
return builder;
}
// Default handling code is for old builders that incorrectly export `default` with non-ESM module
if (builder?.default[BuilderSymbol]) {
return builder.default;
}
throw new Error('Builder is not a builder');
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function getBuilder(builderPath: string): Promise<any> {
const builder = await import(builderPath);
return 'default' in builder ? builder.default : builder;
}
View on GitHub (pinned to bb72145f9a)
Solutions
- Export the builder via createBuilder() and use `export default` in the builder's entry file
- Check the builders.json implementation path resolves to the module that actually default-exports the createBuilder result
- Rebuild the builder with a correct TS/bundler config (esModuleInterop, commonjs settings) so exports are preserved
- Upgrade the builder package if it predates the createBuilder/BuilderSymbol contract
Example fix
// before
export function myBuilder(options, context) { ... }
// after
import { createBuilder } from '@angular-devkit/architect';
export default createBuilder(myBuilder); Defensive patterns
Strategy: type-guard
Validate before calling
const mod = await import(builderPath);
const exp = mod?.default ?? mod;
if (typeof exp !== 'function' || !exp[BuilderSymbol]) {
throw new Error(`${builderPath} does not default-export a createBuilder() result`);
} Type guard
function isBuilder(v) {
return typeof v === 'function' && v[BuilderSymbol] === true;
} Try / catch
try {
const builder = await host.loadBuilder(info);
} catch (e) {
if (e.message === 'Builder is not a builder') {
console.error(`Wrap the export in createBuilder(): export default createBuilder(fn) in ${info.info?.implementation || 'the builder module'}`);
}
throw e;
} Prevention
- Always export builders via `export default createBuilder(fn)`
- Keep a smoke test that imports each builder module and asserts the BuilderSymbol
- Check bundler/TS output (dist) preserves the default export and symbol
- Pin a recent @angular-devkit/architect version compatible with your builder's export style
When it happens
Trigger: resolveBuilder/loadBuilder imports a builder path whose module exports a plain function, an object without the symbol, or a default export that is not the result of createBuilder().
Common situations: Builders written as plain exported functions instead of createBuilder(fn); bundling/transpilation that strips or mangles exports; CommonJS/ESM interop where default wraps the real export incorrectly; pointing at the wrong file (index vs implementation).
Related errors
- The builder requires a target.
- Could not find the '${builderConf}' builder's node package.
- Circular builder alias references detected:
- No builder name specified.
- Package ${JSON.stringify(packageName)} has no builders defin
AI-assisted analysis of angular/angular-cli@bb72145f9a (2026-08-30).
Data as JSON: /api/errors/c253d099617906f1.
Report an issue: GitHub.