evanw/esbuild · error
Cannot watch a disposed context
Error message
Cannot watch a disposed context
What it means
Returned by internalContext.Watch (pkg/api/api_impl.go) when you call Watch() on a context whose didDispose flag is already true. esbuild contexts are single-use after disposal — once Dispose() runs, all further operations on that context are rejected. The guard is intentional so that a disposed context fails fast rather than silently no-op'ing.
Source
Thrown at pkg/api/api_impl.go:1079
return *build
}
// Otherwise, fall back to rebuilding
ctx.mutex.Unlock()
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),
}View on GitHub (pinned to 6ff1d8b0d8)
Solutions
- Create a fresh context with api.Context(options) and call Watch() on the new one after disposing the old.
- Track whether you've disposed the context and skip/guard the Watch call accordingly.
- Reorder teardown so Watch() is never called after Dispose() (stop watching first, then dispose).
- Use a single long-lived context for watch mode and avoid dispose/recreate cycles.
Example fix
// before
ctx.Dispose();
ctx.Watch({}); // -> Cannot watch a disposed context
// after
ctx.Dispose();
ctx, _ = api.Context(opts);
ctx.Watch({}); Defensive patterns
Strategy: validation
Validate before calling
let disposed = false;
function safeWatch(ctx, opts) { if (disposed) throw new Error('context disposed'); ctx.watch(opts); }
// set disposed = true after ctx.dispose() Type guard
function isDisposedError(e) { return e?.message === 'Cannot watch a disposed context'; } Try / catch
try { ctx.Watch(opts); } catch (e) { if (isDisposedError(e)) { ctx = await api.Context(opts2); ctx.Watch(opts); } else throw e; } Prevention
- Track context disposal in a flag and guard Watch().
- Create a new context after Dispose() rather than reusing.
- Stop watching before disposing.
- Centralize context lifecycle in a single manager.
When it happens
Trigger: Calling ctx.Watch(opts) on an esbuild context after ctx.Dispose() has already been invoked on it. The mutex-guarded didDispose check at the top of Watch triggers the error.
Common situations: Restarting watch mode by disposing and re-Watch()'ing the same context instead of creating a new one; a cleanup handler that disposes the context and a later code path tries to (re)start watching; incorrect ordering in a dev-server teardown/restart flow.
Related errors
- Watch mode has already been enabled
- Cannot serve a disposed context
- The service was stopped
- Serve mode has already been enabled
- You need to wait for the promise returned from "initialize"
AI-assisted analysis of evanw/esbuild@6ff1d8b0d8 (2026-08-03).
Data as JSON: /data/errors/57c91f34ede29fa0.json.
Report an issue: GitHub.