evanw/esbuild · error
The service was stopped
Error message
The service was stopped
What it means
Returned from an OnEnd plugin callback running inside the esbuild child-process service (cmd/esbuild run with --service, spawned by esbuild's JS API). It fires when service.sendRequest() fails its type assertion to map[string]interface{}, which happens when the in-memory request channel never receives a real response because the JS host process has closed the pipe / torn down the service. The callback can no longer relay build-end results back to JS, so it short-circuits with this sentinel error. It is esbuild's way of aborting plugin callbacks that are stranded after a context dispose or process exit.
Source
Thrown at cmd/esbuild/service.go:740
//
// This is especially important if "write" is false since otherwise
// we'd unnecessarily send the entire contents of all output files!
//
// "If a tree falls in a forest and no one is
// around to hear it, does it make a sound?"
//
activeBuild.mutex.Lock()
isWithinRebuild := activeBuild.withinRebuildCount > 0
activeBuild.mutex.Unlock()
if !hasOnEndCallbacks && !isWithinRebuild && !writeToStdout {
return api.OnEndResult{}, nil
}
request := resultToResponse(*result)
request["command"] = "on-end"
request["key"] = key
response, ok := service.sendRequest(request).(map[string]interface{})
if !ok {
return api.OnEndResult{}, errors.New("The service was stopped")
}
var errors []api.Message
var warnings []api.Message
if value, ok := response["errors"].([]interface{}); ok {
errors = decodeMessages(value)
}
if value, ok := response["warnings"].([]interface{}); ok {
warnings = decodeMessages(value)
}
return api.OnEndResult{
Errors: errors,
Warnings: warnings,
}, nil
})
},
})
ctx, err := api.Context(options)View on GitHub (pinned to 6ff1d8b0d8)
Solutions
- Ensure you do not call context.dispose() (or let the process exit) while a build is still running — await the rebuild() promise before disposing.
- If tearing down on purpose, treat 'The service was stopped' as expected and swallow it in your plugin/teardown path rather than surfacing it.
- Guard plugin onEnd logic so it degrades gracefully if the result indicates shutdown (the error is informational, not a build failure).
- If unexpected, check for an unhandled exception in your JS-side onEnd callback that is crashing the service.
Example fix
// before const result = await ctx.rebuild(); ctx.dispose(); // may race with pending onEnd plugin relay // after const result = await ctx.rebuild(); // let onEnd callbacks finish before disposing ctx.dispose();
Defensive patterns
Strategy: try-catch
Validate before calling
// Ensure no in-flight build before disposing
let pending = null;
async function safeDispose(ctx) {
if (pending) await pending.catch(() => {});
ctx.dispose();
}
pending = ctx.rebuild();
await safeDispose(ctx); Type guard
function isServiceStoppedError(e) {
return e && typeof e.message === 'string' && e.message === 'The service was stopped';
} Try / catch
try { await ctx.rebuild(); } catch (e) { if (!isServiceStoppedError(e)) throw e; /* expected during teardown */ } Prevention
- Always await the current build promise before calling dispose().
- Treat 'The service was stopped' as an expected shutdown signal, not a bug.
- Avoid churning esbuild contexts; reuse one long-lived context.
- Ensure JS-side plugin callbacks never throw uncaught errors.
When it happens
Trigger: A Go-shim OnEnd callback (registered via build.OnEnd in the service bridge) calls service.sendRequest({command:"on-end"...}) and the returned value is not a map — i.e. the JS side never replied. This occurs when ctx.Dispose() (or process kill / unhandled rejection) happens while a build with plugins is still finishing, or when the stdio connection to the JS parent is severed mid-build.
Common situations: Calling esbuild context .dispose() while a build is in flight and the build has an onEnd plugin; the JS host crashing or being SIGKILLed during a build; hot-reload tooling that disposes and recreates esbuild contexts rapidly; CI timeouts killing the esbuild process mid-build.
Related errors
- Cannot watch a disposed context
- Watch mode has already been enabled
- Invalid path suffix %q returned from plugin (must start with
- Cannot serve a disposed context
- Serve mode has already been enabled
AI-assisted analysis of evanw/esbuild@6ff1d8b0d8 (2026-08-03).
Data as JSON: /data/errors/602737caabe28e69.json.
Report an issue: GitHub.