angular/angular-cli · error
The loader "${filename}" didn't return a string.
Error message
The loader "${filename}" didn't return a string. What it means
After running a webpack loader chain in the child compilation, the loader evaluates the result (`_evaluate`) and expects the module's export to be a string (or a `{ default: string }` ES-module wrapper). If the final loader output is neither, it throws — the loader chain did not produce the expected stringified resource.
Source
Thrown at packages/ngtools/webpack/src/resource_loader.ts:341
btoa(input) {
return Buffer.from(input).toString('base64');
},
};
try {
vm.runInNewContext(source, context, { filename });
} catch {
// Error are propagated through the child compilation.
return null;
}
if (typeof context.resource === 'string') {
return context.resource;
} else if (typeof context.resource?.default === 'string') {
return context.resource.default;
}
throw new Error(`The loader "${filename}" didn't return a string.`);
}
async get(filePath: string): Promise<string> {
const normalizedFile = normalizePath(filePath);
let compilationResult = this.fileCache?.get(normalizedFile);
if (compilationResult === undefined) {
// cache miss so compile resource
compilationResult = await this._compile(filePath);
// Only cache if compilation was successful
if (this.fileCache && compilationResult.success) {
this.fileCache.set(normalizedFile, compilationResult);
}
}
return compilationResult.content;
}View on GitHub (pinned to bb72145f9a)
Solutions
- Ensure the LAST loader in the chain returns a string (call `.toString()` on Buffers)
- Check the matching rule for templates/styles in webpack config and fix/remove loaders emitting objects
- If the loader exports an object with a string default, make sure it is exposed as `module.exports.default` or `module.exports = 'string'`
- Pin/align loader versions after upgrades (e.g. html-loader major bumps) and re-test
Example fix
// before
module.exports = { html: compiledHtml }; // loader returns an object
// after
module.exports = compiledHtml; // or module.exports.default = compiledHtml; Defensive patterns
Strategy: type-guard
Validate before calling
function loaderReturnsString(loaderChain) {
// ensure the final loader converts Buffers and exports plain strings
return loaderChain.every((l) => !l.emitsObjects);
}
// in the custom loader: this.callback(null, content.toString()); Type guard
function isStringResource(v) {
return typeof v === 'string' || (v != null && typeof v.default === 'string');
} Try / catch
try {
const content = await loader.get(filePath);
if (!isStringResource(content)) throw new TypeError('loader produced non-string resource');
} catch (e) {
if (e.message.includes("didn't return a string")) {
console.error('Check the last loader in the chain for', filePath);
}
throw e;
} Prevention
- Guarantee the final loader in each template/style rule returns a string
- Call .toString() on Buffers before returning loader output
- After swapping/upgrading loaders, run a smoke build covering templates and styles
When it happens
Trigger: A custom/alternative loader for a template or style returns an object, Buffer, promise, or undefined instead of a string, e.g. a misconfigured raw-loader/html-loader emitting `{ html: ... }` or a loader doing `module.exports = {...}`. Raised from `output()` invoked via `get()`.
Common situations: Upgrading or swapping loaders (e.g. replacing raw-loader) where the new loader returns structured metadata; custom loaders forgetting `this.callback(null, content.toString())`; a loader returning a Buffer that isn't converted.
Related errors
- Webpack stats build result is required.
- The "application" and "browser-esbuild" builders do not supp
- Only the "application" and "browser-esbuild" builders suppor
- Only the "application" and "browser-esbuild" builders suppor
- Webpack Dev Server configuration was not set.
AI-assisted analysis of angular/angular-cli@bb72145f9a (2026-08-30).
Data as JSON: /api/errors/f100f3d7d9a97356.
Report an issue: GitHub.