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

  1. Ensure the LAST loader in the chain returns a string (call `.toString()` on Buffers)
  2. Check the matching rule for templates/styles in webpack config and fix/remove loaders emitting objects
  3. If the loader exports an object with a string default, make sure it is exposed as `module.exports.default` or `module.exports = 'string'`
  4. 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

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


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