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

  1. Create a fresh context with api.Context(options) and call Watch() on the new one after disposing the old.
  2. Track whether you've disposed the context and skip/guard the Watch call accordingly.
  3. Reorder teardown so Watch() is never called after Dispose() (stop watching first, then dispose).
  4. 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

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


AI-assisted analysis of evanw/esbuild@6ff1d8b0d8 (2026-08-03). Data as JSON: /data/errors/57c91f34ede29fa0.json. Report an issue: GitHub.