Skip to main content

What is a Loader?

Loaders are transformations applied to the source code of a module. They allow you to pre-process files as you import or “load” them. Loaders can transform files from different languages (like TypeScript) to JavaScript, or inline images as data URLs.

How Loaders Work

Loaders are executed from right to left (or bottom to top) in a chain. Each loader in the chain applies transformations to the processed resource. The final loader is expected to return JavaScript.
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',  // 3. Inject CSS into DOM
          'css-loader',    // 2. Turns CSS into JS
          'sass-loader'    // 1. Turns Sass into CSS
        ]
      }
    ]
  }
};

Basic Loader Structure

A loader is a JavaScript module that exports a function:
module.exports = function(source) {
  // source is the content of the resource file
  // Transform the source and return it
  return source.replace(/const/g, 'var');
};
The this context in a loader is provided by webpack and contains useful methods and properties known as the LoaderContext API.

Loader Execution Phases

Loaders run in two phases:

1. Pitching Phase

Loaders are executed from left to right (or top to bottom) during the pitching phase. Pitching loaders can short-circuit the loader chain.

2. Normal Phase

Loaders are executed from right to left (or bottom to top) during the normal phase, processing the actual source code.
loader1.pitch → loader2.pitch → loader3.pitch

loader1 ← loader2 ← loader3 ← resource content
See Pitching Loaders for detailed information.

Synchronous vs Asynchronous Loaders

Synchronous Loaders

Return the transformed source directly:
module.exports = function(source) {
  const transformed = transform(source);
  return transformed;
};

Asynchronous Loaders

Use this.async() for async operations:
module.exports = function(source) {
  const callback = this.async();
  
  someAsyncOperation(source, (err, result) => {
    if (err) return callback(err);
    callback(null, result);
  });
};

Promise-based Loaders

Return a Promise for async operations:
module.exports = async function(source) {
  const result = await someAsyncOperation(source);
  return result;
};

Raw Loaders

By default, loaders receive the resource file as a UTF-8 string. Set raw to true to receive raw Buffer:
module.exports = function(source) {
  // source is a Buffer
  assert(Buffer.isBuffer(source));
  return doSomething(source);
};

module.exports.raw = true;
Raw loaders are useful for processing binary files like images, fonts, or other non-text assets.

Returning Multiple Values

Loaders can return source maps and additional metadata using this.callback():
module.exports = function(source, sourceMap, meta) {
  // Transform the source
  const result = transform(source);
  
  // Return result with source map
  this.callback(null, result, sourceMap, meta);
};

Common Loader Patterns

Basic Transformation

module.exports = function(source) {
  return source.replace(/foo/g, 'bar');
};

With Options

module.exports = function(source) {
  const options = this.getOptions();
  
  return transform(source, options);
};

With Validation

const schema = {
  type: 'object',
  properties: {
    test: {
      type: 'boolean'
    }
  }
};

module.exports = function(source) {
  const options = this.getOptions(schema);
  
  return transform(source, options);
};

Emitting Files

module.exports = function(source) {
  const url = interpolateName(this, '[hash].[ext]', { source });
  
  this.emitFile(url, source);
  
  return `export default ${JSON.stringify(url)}`;
};

Adding Dependencies

Make webpack watch additional files:
module.exports = function(source) {
  // Watch this file for changes
  this.addDependency('./config.json');
  
  // Watch this directory
  this.addContextDependency('./templates');
  
  return transform(source);
};

Caching

Loaders are cacheable by default. Opt out if your loader has external dependencies:
module.exports = function(source) {
  // Disable caching
  this.cacheable(false);
  
  const config = fs.readFileSync('./config.json');
  return transform(source, config);
};
Instead of disabling caching, use this.addDependency() to add external files as dependencies.

Error Handling

Synchronous Errors

module.exports = function(source) {
  try {
    return transform(source);
  } catch (error) {
    throw error; // or this.callback(error)
  }
};

Asynchronous Errors

module.exports = function(source) {
  const callback = this.async();
  
  someAsyncOperation(source, (err, result) => {
    if (err) return callback(err);
    callback(null, result);
  });
};

Warnings

module.exports = function(source) {
  if (deprecated(source)) {
    this.emitWarning(new Error('Deprecated syntax detected'));
  }
  
  return transform(source);
};

Loader Context

Loaders have access to many useful methods via this:
  • this.addDependency(file) - Add a file dependency
  • this.async() - Make the loader async
  • this.callback() - Return multiple values
  • this.emitFile(name, content) - Emit a file
  • this.emitWarning(warning) - Emit a warning
  • this.emitError(error) - Emit an error
  • this.getOptions() - Get loader options
  • this.resolve() - Resolve a request
See the complete LoaderContext API reference.

Real-World Example: Markdown Loader

const marked = require('marked');

const schema = {
  type: 'object',
  properties: {
    pedantic: { type: 'boolean' },
    gfm: { type: 'boolean' }
  }
};

module.exports = function(source) {
  const options = this.getOptions(schema);
  
  // Mark this loader as cacheable
  this.cacheable();
  
  // Configure marked
  marked.setOptions(options);
  
  // Transform markdown to HTML
  const html = marked.parse(source);
  
  // Return as JavaScript module
  return `export default ${JSON.stringify(html)}`;
};

Guidelines

1. Keep Loaders Simple

Each loader should do one thing well. Chain multiple loaders for complex transformations.

2. Use Module Utilities

Leverage webpack’s utilities like this.resolve() instead of Node.js require.resolve().

3. Mark Dependencies

Always use this.addDependency() for external files to enable proper watching.

4. Resolve Modules Relatively

Use this.resolve() to resolve modules relative to the current module.

5. Extract Common Code

Extract common code into a runtime module using this.emitFile().

6. Avoid Absolute Paths

Generate relative paths or use webpack’s utilities to make loaders portable.

7. Use Peer Dependencies

If your loader depends on a library, list it as a peerDependency.

Testing Loaders

const compiler = webpack({
  entry: './test-file.js',
  module: {
    rules: [
      {
        test: /\.txt$/,
        use: {
          loader: path.resolve(__dirname, './my-loader.js'),
          options: {
            /* options */
          }
        }
      }
    ]
  }
});

compiler.run((err, stats) => {
  // Test assertions
});

Next Steps

Further Reading