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

  1. Export the builder via createBuilder() and use `export default` in the builder's entry file
  2. Check the builders.json implementation path resolves to the module that actually default-exports the createBuilder result
  3. Rebuild the builder with a correct TS/bundler config (esModuleInterop, commonjs settings) so exports are preserved
  4. 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

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


AI-assisted analysis of angular/angular-cli@bb72145f9a (2026-08-30). Data as JSON: /api/errors/c253d099617906f1. Report an issue: GitHub.