evanw/esbuild · critical

Invalid loader

Error message

Invalid loader

What it means

validateLoader (pkg/api/api_impl.go:299) panics when a Loader value is not one of LoaderNone..LoaderTSX (the 17-constant iota in api.go). The function maps the public Loader to the internal config.Loader used for parsing a file extension or stdin; an unknown value means the parser cannot be selected, so esbuild aborts. Called from validateLoaders (per extension in BuildOptions.Loader), from StdinOptions.Loader, and from TransformOptions.Loader.

Solutions

  1. Use only the api.Loader* constants; for unknown extensions default to api.LoaderDefault or api.LoaderNone.
  2. Bounds-check decoded integers against 0..16 (LoaderTSX) before casting.
  3. Store loader mappings keyed by name ("js","ts","css",...) and translate to constants at load.
  4. After re-vendoring esbuild, regenerate any bridge/table code (js_table.ts) and rebuild all consumers.

Example fix

// before
opts := api.BuildOptions{Loader: map[string]api.Loader{".custom": api.Loader(40)}}

// after
opts := api.BuildOptions{Loader: map[string]api.Loader{".custom": api.LoaderText}}
Defensive patterns

Strategy: validation

Validate before calling

func checkLoader(l api.Loader) error {
    switch l {
    case api.LoaderNone, api.LoaderBase64, api.LoaderBinary, api.LoaderCopy,
        api.LoaderCSS, api.LoaderDataURL, api.LoaderDefault, api.LoaderEmpty,
        api.LoaderFile, api.LoaderGlobalCSS, api.LoaderJS, api.LoaderJSON,
        api.LoaderJSX, api.LoaderLocalCSS, api.LoaderText, api.LoaderTS, api.LoaderTSX:
        return nil
    }
    return fmt.Errorf("invalid loader %d (want 0..%d)", uint16(l), uint16(api.LoaderTSX))
}

for ext, l := range opts.Loader {
    if err := checkLoader(l); err != nil { return fmt.Errorf("loader for %q: %w", ext, err) }
}
if opts.Stdin != nil {
    if err := checkLoader(opts.Stdin.Loader); err != nil { return err }
}

Type guard

func isValidLoader(l api.Loader) bool {
    switch l {
    case api.LoaderNone, api.LoaderBase64, api.LoaderBinary, api.LoaderCopy,
        api.LoaderCSS, api.LoaderDataURL, api.LoaderDefault, api.LoaderEmpty,
        api.LoaderFile, api.LoaderGlobalCSS, api.LoaderJS, api.LoaderJSON,
        api.LoaderJSX, api.LoaderLocalCSS, api.LoaderText, api.LoaderTS, api.LoaderTSX:
        return true
    }
    return false
}

Prevention

When it happens

Trigger: Passing an out-of-range integer for any Loader field, deserializing Loader maps from a numeric wire format, or bridging a different esbuild version where the iota ordering or count differs (this enum is the longest of the set, so it is the most sensitive to renumbering).

Common situations: Re-vendoring esbuild and forgetting to regenerate a JS/Go bridge that maps loader names to numbers, persisted configs that store loader as an int, or a typo like `LoaderJSON5` (no such constant). Also triggered by a `Loader` map entry whose value was set to a stale constant after an upgrade removed it.

Related errors


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

Appendix: source

Thrown at pkg/api/api_impl.go:299

		return config.LoaderGlobalCSS
	case LoaderJS:
		return config.LoaderJS
	case LoaderJSON:
		return config.LoaderJSON
	case LoaderJSX:
		return config.LoaderJSX
	case LoaderLocalCSS:
		return config.LoaderLocalCSS
	case LoaderNone:
		return config.LoaderNone
	case LoaderText:
		return config.LoaderText
	case LoaderTS:
		return config.LoaderTS
	case LoaderTSX:
		return config.LoaderTSX
	default:
		panic("Invalid loader")
	}
}

func extractPathStyle(absPaths AbsPaths, flag AbsPaths) logger.PathStyle {
	if (absPaths & flag) != 0 {
		return logger.AbsPath
	} else {
		return logger.RelPath
	}
}

var versionRegex = regexp.MustCompile(`^([0-9]+)(?:\.([0-9]+))?(?:\.([0-9]+))?(-[A-Za-z0-9]+(?:\.[A-Za-z0-9]+)*)?$`)

func validateFeatures(log logger.Log, target Target, engines []Engine) (compat.JSFeature, compat.CSSFeature, map[css_ast.D]compat.CSSPrefix, string) {
	if target == DefaultTarget && len(engines) == 0 {
		return 0, 0, nil, ""
	}

View on GitHub (pinned to f6058f8364)