evanw/esbuild · error

Serve mode has already been enabled

Error message

Serve mode has already been enabled

What it means

Returned by internalContext.Serve when ctx.handler is already non-nil, meaning a serve HTTP server is already running on this context. esbuild permits at most one serve instance per context; calling Serve() again is a programming error. The guard prevents binding a second listener / conflicting request handlers.

Source

Thrown at pkg/api/serve_other.go:741

func prettyPrintPath(fs fs.FS, path string) string {
	if relPath, ok := fs.Rel(fs.Cwd(), path); ok {
		return strings.ReplaceAll(relPath, "\\", "/")
	}
	return path
}

func (ctx *internalContext) Serve(serveOptions ServeOptions) (ServeResult, error) {
	ctx.mutex.Lock()
	defer ctx.mutex.Unlock()

	// Ignore disposed contexts
	if ctx.didDispose {
		return ServeResult{}, errors.New("Cannot serve a disposed context")
	}

	// Don't allow starting serve mode multiple times
	if ctx.handler != nil {
		return ServeResult{}, errors.New("Serve mode has already been enabled")
	}

	// Don't allow starting serve mode multiple times
	if (serveOptions.Keyfile != "") != (serveOptions.Certfile != "") {
		return ServeResult{}, errors.New("Must specify both key and certificate for HTTPS")
	}

	// Validate the "servedir" path
	if serveOptions.Servedir != "" {
		if absPath, ok := ctx.realFS.Abs(serveOptions.Servedir); ok {
			serveOptions.Servedir = absPath
		} else {
			return ServeResult{}, fmt.Errorf("Invalid serve path: %s", serveOptions.Servedir)
		}
	}

	// Validate the "fallback" path
	if serveOptions.Fallback != "" {

View on GitHub (pinned to 6ff1d8b0d8)

Solutions

  1. Call Serve() once per context; track whether serving has started.
  2. To change serve options (port, servedir, TLS), dispose the context, create a new one, and Serve() again.
  3. Centralize the Serve() call so only one code path can invoke it.
  4. Guard with a flag: if (!serving) { ctx.Serve(...); serving = true; }

Example fix

// before
await ctx.Serve({ port: 8000 });
await ctx.Serve({ port: 8001 }); // -> Serve mode has already been enabled

// after
await ctx.Serve({ port: 8000 });
// to change: recreate the context
ctx.Dispose();
ctx = await api.Context(opts);
await ctx.Serve({ port: 8001 });
Defensive patterns

Strategy: validation

Validate before calling

let serving = false;
function startServe(ctx, opts) { if (serving) throw new Error('already serving'); serving = true; return ctx.serve(opts); }

Type guard

function isAlreadyServingError(e) { return e?.message === 'Serve mode has already been enabled'; }

Try / catch

try { await ctx.Serve(opts); } catch (e) { if (isAlreadyServingError(e)) return; throw e; }

Prevention

When it happens

Trigger: Calling ctx.Serve(opts) twice on the same context without disposing in between. The second call observes ctx.handler != nil and returns the error.

Common situations: A dev server that calls Serve() on every reload without checking; two modules both trying to start serving on a shared context; port-change logic that re-invokes Serve instead of recreating the context.

Related errors


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