{"record":{"id":"4e6e53769d60a354","repo":"different-ai/openwork","slug":"unhandled-rejection-from-an-un-awaited-tool-call","errorCode":null,"errorMessage":"Unhandled rejection from an un-awaited tool call: ${failure.message}","messagePattern":"Unhandled rejection from an un-awaited tool call: (.+?)","errorType":"exception","errorClass":"InterpreterRuntimeError","httpStatus":null,"severity":"error","filePath":"packages/codemode/src/interpreter/runtime.ts","lineNumber":717,"sourceCode":"      // without an explicit await, exactly as in JS.\n      if (value instanceof SandboxPromise) value = yield* self.settlePromise(value)\n      yield* self.drainPendingSettlements()\n      return value\n    }).pipe(Effect.ensuring(Effect.sync(() => self.popScope())))\n  }\n\n  // Awaits every fiber-backed promise the program abandoned (fire-and-forget tool calls), so\n  // their work completes before the execution ends - mirroring a JS runtime waiting on\n  // in-flight I/O at exit. A failure nobody could have handled becomes an unhandled-rejection\n  // diagnostic (interrupted calls, e.g. Promise.race losers, are ignored).\n  private drainPendingSettlements(): Effect.Effect<void, unknown, never> {\n    const self = this\n    return Effect.gen(function* () {\n      for (const promise of [...self.pendingSettlements]) {\n        const exit = yield* self.observePromise(promise)\n        if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause)) continue\n        const failure = normalizeError(Cause.squash(exit.cause))\n        throw new InterpreterRuntimeError(\n          `Unhandled rejection from an un-awaited tool call: ${failure.message}`,\n          undefined,\n          failure.kind,\n          [\"Await tool calls - `const result = await tools.ns.tool(...)` - so failures can be caught and handled.\"],\n        )\n      }\n    })\n  }\n\n  // Eagerly starts a tool call on a supervised child fiber (so the execution timeout and\n  // scope teardown interrupt it) gated by the concurrency semaphore, and wraps the fiber in a\n  // first-class promise value. `startImmediately` makes the runtime admit the call - charging\n  // the tool-call budget and firing onToolCallStart - at the call site, before any await.\n  private createToolCallPromise(\n    path: ReadonlyArray<string>,\n    args: Array<unknown>,\n  ): Effect.Effect<SandboxPromise, never, R> {\n    const self = this","sourceCodeStart":699,"sourceCodeEnd":735,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/packages/codemode/src/interpreter/runtime.ts#L699-L735","documentation":"CodeMode allows fire-and-forget tool calls, but at the end of execution drainPendingSettlements inspects every still-pending promise. If any un-awaited tool call rejected (and wasn't interrupted), the failure is rethrown as this error so silent failures cannot be swallowed. The hint tells you to await tool calls so failures can be caught.","triggerScenarios":"Calling tools.ns.tool(...) without await and the underlying tool call fails; the rejection surfaces only after the program finishes, via drainPendingSettlements invoked from run.","commonSituations":"Omitting await on tool calls that return promises; assuming un-awaited tool errors are logged rather than fatal; LLM-generated code missing await; fire-and-forget patterns like void tools.http.get(...) that then fail.","solutions":["Await every tool call: const result = await tools.ns.tool(...) and wrap in try/catch if failure is expected","If fire-and-forget is intended, ensure the call cannot fail or attach a catch handler supported by the runtime","Check the failure.kind in the error to identify which tool failed and fix its inputs"],"exampleFix":"// before\ntools.http.get(url); // un-awaited; failure surfaces at end of run\n// after\nconst result = await tools.http.get(url);","handlingStrategy":"try-catch","validationCode":"// Prefer awaiting every tool call; flag un-awaited calls statically\nfor (const m of code.matchAll(/(^|[^.\\w])tools\\.\\w+\\.\\w+\\s*\\(/g)) {\n  const prefix = code.slice(Math.max(0, m.index - 12), m.index + 12);\n  if (!/await\\s*$/.test(code.slice(0, m.index).trimEnd()) && !/\\bawait\\b/.test(prefix)) {\n    console.warn('possible un-awaited tool call:', prefix.trim());\n  }\n}","typeGuard":"function isAwaitedToolCall(stmt) { return /await\\s+tools\\./.test(stmt); }","tryCatchPattern":"try {\n  result = yield* interpreter.run(code);\n} catch (e) {\n  if (String(e.message).startsWith('Unhandled rejection from an un-awaited tool call')) {\n    // identify the failing tool from failure.kind and add await + try/catch in code\n  } else throw e;\n}","preventionTips":["Always write const result = await tools.ns.tool(...) for tool calls","Wrap fallible tool calls in try/catch inside the sandbox code","Never rely on fire-and-forget tool calls for required side effects","Instruct code-generating models to await all tool calls"],"tags":["codemode","unhandled-rejection","promise","tool-call","async"],"backgroundTag":"unhandled-promise-rejection","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}