evanw/esbuild · error
Watch mode has already been enabled
Error message
Watch mode has already been enabled
What it means
esbuild contexts support only one active watcher at a time. This error fires when Watch() is called on a context that already has a non-nil watcher field, indicating watch mode is already running.
Solutions
- Call watch() only once per context lifecycle
- Create separate contexts for separate watch configurations
- Dispose and recreate the context if you need to restart watch mode with different options
Example fix
// before
await ctx.watch({})
await ctx.watch({ delay: 500 })
// after
await ctx.dispose()
const ctx2 = await esbuild.context(buildOptions)
await ctx2.watch({ delay: 500 }) Defensive patterns
Strategy: validation
Validate before calling
class ManagedContext {
constructor(ctx) { this.ctx = ctx; this.watching = false }
async watch(opts) {
if (this.watching) throw new Error('Watch already started')
this.watching = true
return this.ctx.watch(opts)
}
} Prevention
- Call watch() exactly once per context from a single initialization point
- Guard against multiple initialization paths that could each call watch
- Use a wrapper class to track whether watch has already been called
When it happens
Trigger: Calling ctx.watch() twice without disposing the context in between.
Common situations: Multiple code paths that each call watch (e.g., initialization and config-change handler), or calling watch in a loop.
Related errors
- Cannot watch a disposed context
- Cannot serve a disposed context
- Serve mode has already been enabled
- Cannot call "resolve" before plugin setup has completed
- Cannot compute relative path from
AI-assisted analysis of evanw/esbuild@f6058f8364 (2026-08-09).
Data as JSON: /api/errors/e93055fc54838f28.
Report an issue: GitHub.
Appendix: source
Thrown at pkg/api/api_impl.go:1105
return ctx.Rebuild()
}
func (ctx *internalContext) Rebuild() BuildResult {
return ctx.rebuild().result
}
func (ctx *internalContext) Watch(options WatchOptions) error {
ctx.mutex.Lock()
defer ctx.mutex.Unlock()
// Ignore disposed contexts
if ctx.didDispose {
return errors.New("Cannot watch a disposed context")
}
// Don't allow starting watch mode multiple times
if ctx.watcher != nil {
return errors.New("Watch mode has already been enabled")
}
logLevel := ctx.args.logOptions.LogLevel
ctx.watcher = &watcher{
fs: ctx.realFS,
shouldLog: logLevel == logger.LevelInfo || logLevel == logger.LevelDebug || logLevel == logger.LevelVerbose,
useColor: ctx.args.logOptions.Color,
pathStyle: ctx.args.logOptions.PathStyle,
rebuild: func() fs.WatchData {
return ctx.rebuild().watchData
},
delayInMS: time.Duration(options.Delay),
}
// All subsequent builds will be watch mode builds
ctx.args.options.WatchMode = true
// Start the file watcher goroutineView on GitHub (pinned to f6058f8364)