diff --git a/cmd/tsconnect/src/lib/start-ipn.ts b/cmd/tsconnect/src/lib/start-ipn.ts index 9b364548e..378297f5e 100644 --- a/cmd/tsconnect/src/lib/start-ipn.ts +++ b/cmd/tsconnect/src/lib/start-ipn.ts @@ -27,15 +27,29 @@ export async function startIPN( }) go.env[INIT_CALLBACK_ENV] = name - const exited = go.run(instance).then(() => { + + // An exit before the IPN reaches the caller is a startup failure, and throwing + // hands it back as a rejection. Afterwards the caller holds the only shutdown + // path, so an exit is either that shutdown or a panic; report it either way, + // because the IPN is dead in both cases. + let handedOver = false + const exited: Promise = go.run(instance).then(() => { delete globals[name] - onExit("Unexpected shutdown") - throw new Error("Go runtime exited before the IPN was ready") + if (handedOver) onExit("Go runtime exited") + // Always reject: before the handover this is what fails the race below, + // and after it the race has settled, so nothing observes the rejection. + throw new Error( + handedOver + ? "Go runtime exited" + : "Go runtime exited before the IPN was ready" + ) }) const [newIPN, terminate] = await Promise.race([ready, exited]) try { - return await newIPN(config) + const ipn = await newIPN(config) + handedOver = true + return ipn } catch (err) { // Nothing was built, so nothing can shut the runtime down. Exit it here and // wait for it, or the page keeps a blocked runtime for a failed startup.