evanw/esbuild · error · Error

onLoad() call is missing a filter

Error message

onLoad() call is missing a filter

What it means

When a plugin registers an `onLoad` callback (lib/shared/common.ts:1327), esbuild reads `options.filter` with `mustBeRegExp` and throws if it is null/undefined. As with onResolve, the filter limits which module paths the load callback handles and is required.

Solutions

  1. Pass a `filter` RegExp, e.g. `build.onLoad({ filter: /\.css$/, namespace: 'file' }, cb)`.
  2. Use `/.*/` only when you genuinely need every module and understand the cost.
  3. Ensure the value is a RegExp, not a string glob.

Example fix

// before
build.onLoad({ namespace: 'ns' }, args => { ... });
// after
build.onLoad({ filter: /.*/, namespace: 'ns' }, args => { ... });
Defensive patterns

Strategy: validation

Validate before calling

function registerOnLoad(build, opts, cb) {
  if (!(opts.filter instanceof RegExp)) throw new Error('onLoad requires a filter RegExp');
  build.onLoad(opts, cb);
}

Type guard

function hasOnLoadFilter(o: any): o is { filter: RegExp } {
  return !!o && o.filter instanceof RegExp;
}

Prevention

When it happens

Trigger: Calling `build.onLoad({}, callback)` or `build.onLoad({ namespace: 'ns' }, callback)` without a `filter` RegExp.

Common situations: Forgetting the filter; passing a glob string instead of a RegExp; copying an onResolve registration and dropping the filter.

Related errors


AI-assisted analysis of evanw/esbuild@f6058f8364 (2026-08-09). Data as JSON: /api/errors/1b80e995ad53fd1f. Report an issue: GitHub.

Appendix: source

Thrown at lib/shared/common.ts:1327

          let registeredNote = extractCallerV8(new Error(registeredText), streamIn, 'onResolve')
          let keys: OptionKeys = {}
          let filter = getFlag(options, keys, 'filter', mustBeRegExp)
          let namespace = getFlag(options, keys, 'namespace', mustBeString)
          checkForInvalidFlags(options, keys, `in onResolve() call for plugin ${quote(name)}`)
          if (filter == null) throw new Error(`onResolve() call is missing a filter`)
          let id = nextCallbackID++
          onResolveCallbacks[id] = { name: name!, callback, note: registeredNote }
          plugin.onResolve.push({ id, filter: jsRegExpToGoRegExp(filter), namespace: namespace || '' })
        },

        onLoad(options, callback) {
          let registeredText = `This error came from the "onLoad" callback registered here:`
          let registeredNote = extractCallerV8(new Error(registeredText), streamIn, 'onLoad')
          let keys: OptionKeys = {}
          let filter = getFlag(options, keys, 'filter', mustBeRegExp)
          let namespace = getFlag(options, keys, 'namespace', mustBeString)
          checkForInvalidFlags(options, keys, `in onLoad() call for plugin ${quote(name)}`)
          if (filter == null) throw new Error(`onLoad() call is missing a filter`)
          let id = nextCallbackID++
          onLoadCallbacks[id] = { name: name!, callback, note: registeredNote }
          plugin.onLoad.push({ id, filter: jsRegExpToGoRegExp(filter), namespace: namespace || '' })
        },

        onDispose(callback) {
          onDisposeCallbacks.push(callback)
        },

        esbuild: streamIn.esbuild,
      })

      // Await a returned promise if there was one. This allows plugins to do
      // some asynchronous setup while still retaining the ability to modify
      // the build options. This deliberately serializes asynchronous plugin
      // setup instead of running them concurrently so that build option
      // modifications are easier to reason about.
      if (promise) await promise

View on GitHub (pinned to f6058f8364)