evanw/esbuild · error

Cannot watch a disposed context

Error message

Cannot watch a disposed context

What it means

This error fires when Watch() is called on an esbuild context that has already been disposed via Dispose(). The context tracks a didDispose flag and refuses to start a watcher on a disposed context since all internal state has been torn down.

Solutions

  1. Create a new context with esbuild.context(options) before calling watch after a dispose
  2. Ensure your lifecycle ordering calls watch before dispose
  3. Track your own disposed flag to guard against accidental reuse

Example fix

// before
await ctx.dispose()
await ctx.watch({})
// after
await ctx.dispose()
const ctx2 = await esbuild.context(buildOptions)
await ctx2.watch({})
Defensive patterns

Strategy: try-catch

Validate before calling

class ContextManager {
  constructor(ctx) { this.ctx = ctx; this.disposed = false }
  async watch(opts) {
    if (this.disposed) throw new Error('Context already disposed')
    return this.ctx.watch(opts)
  }
  async dispose() {
    if (this.disposed) return
    this.disposed = true
    await this.ctx.dispose()
  }
}

Try / catch

try {
  await ctx.watch(options)
} catch (e) {
  if (e.message.includes('disposed context')) {
    // recreate context and retry
    ctx = await esbuild.context(buildOptions)
    await ctx.watch(options)
  } else {
    throw e
  }
}

Prevention

When it happens

Trigger: Calling await ctx.dispose() and then await ctx.watch() on the same context object without creating a new one.

Common situations: Lifecycle management bugs in test teardown, hot-reload setups, or config-change handlers where a context is disposed and then accidentally reused.

Related errors


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

Appendix: source

Thrown at pkg/api/api_impl.go:1100

		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 f6058f8364)