{"id":"602737caabe28e69","repo":"evanw/esbuild","slug":"the-service-was-stopped","errorCode":null,"errorMessage":"The service was stopped","messagePattern":"The service was stopped","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/esbuild/service.go","lineNumber":740,"sourceCode":"\t\t\t\t\t//\n\t\t\t\t\t// This is especially important if \"write\" is false since otherwise\n\t\t\t\t\t// we'd unnecessarily send the entire contents of all output files!\n\t\t\t\t\t//\n\t\t\t\t\t//          \"If a tree falls in a forest and no one is\n\t\t\t\t\t//           around to hear it, does it make a sound?\"\n\t\t\t\t\t//\n\t\t\t\t\tactiveBuild.mutex.Lock()\n\t\t\t\t\tisWithinRebuild := activeBuild.withinRebuildCount > 0\n\t\t\t\t\tactiveBuild.mutex.Unlock()\n\t\t\t\t\tif !hasOnEndCallbacks && !isWithinRebuild && !writeToStdout {\n\t\t\t\t\t\treturn api.OnEndResult{}, nil\n\t\t\t\t\t}\n\t\t\t\t\trequest := resultToResponse(*result)\n\t\t\t\t\trequest[\"command\"] = \"on-end\"\n\t\t\t\t\trequest[\"key\"] = key\n\t\t\t\t\tresponse, ok := service.sendRequest(request).(map[string]interface{})\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\treturn api.OnEndResult{}, errors.New(\"The service was stopped\")\n\t\t\t\t\t}\n\t\t\t\t\tvar errors []api.Message\n\t\t\t\t\tvar warnings []api.Message\n\t\t\t\t\tif value, ok := response[\"errors\"].([]interface{}); ok {\n\t\t\t\t\t\terrors = decodeMessages(value)\n\t\t\t\t\t}\n\t\t\t\t\tif value, ok := response[\"warnings\"].([]interface{}); ok {\n\t\t\t\t\t\twarnings = decodeMessages(value)\n\t\t\t\t\t}\n\t\t\t\t\treturn api.OnEndResult{\n\t\t\t\t\t\tErrors:   errors,\n\t\t\t\t\t\tWarnings: warnings,\n\t\t\t\t\t}, nil\n\t\t\t\t})\n\t\t\t},\n\t\t})\n\n\t\tctx, err := api.Context(options)","sourceCodeStart":722,"sourceCodeEnd":758,"githubUrl":"https://github.com/evanw/esbuild/blob/6ff1d8b0d8c134e867a397eef39702a223ebef9e/cmd/esbuild/service.go#L722-L758","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst result = await ctx.rebuild();\nctx.dispose(); // may race with pending onEnd plugin relay\n\n// after\nconst result = await ctx.rebuild();\n// let onEnd callbacks finish before disposing\nctx.dispose();","handlingStrategy":"try-catch","validationCode":"// Ensure no in-flight build before disposing\nlet pending = null;\nasync function safeDispose(ctx) {\n  if (pending) await pending.catch(() => {});\n  ctx.dispose();\n}\npending = ctx.rebuild();\nawait safeDispose(ctx);","typeGuard":"function isServiceStoppedError(e) {\n  return e && typeof e.message === 'string' && e.message === 'The service was stopped';\n}","tryCatchPattern":"try { await ctx.rebuild(); } catch (e) { if (!isServiceStoppedError(e)) throw e; /* expected during teardown */ }","preventionTips":["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."],"tags":["esbuild","plugin","lifecycle","service-bridge"],"analyzedSha":"6ff1d8b0d8c134e867a397eef39702a223ebef9e","analyzedAt":"2026-08-03T19:42:38.433Z","schemaVersion":2}