different-ai/openwork · error · InterpreterRuntimeError

Unhandled rejection from an un-awaited tool call: ${failure.

Error message

Unhandled rejection from an un-awaited tool call: ${failure.message}

What it means

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.

Source

Thrown at packages/codemode/src/interpreter/runtime.ts:717

      // without an explicit await, exactly as in JS.
      if (value instanceof SandboxPromise) value = yield* self.settlePromise(value)
      yield* self.drainPendingSettlements()
      return value
    }).pipe(Effect.ensuring(Effect.sync(() => self.popScope())))
  }

  // Awaits every fiber-backed promise the program abandoned (fire-and-forget tool calls), so
  // their work completes before the execution ends - mirroring a JS runtime waiting on
  // in-flight I/O at exit. A failure nobody could have handled becomes an unhandled-rejection
  // diagnostic (interrupted calls, e.g. Promise.race losers, are ignored).
  private drainPendingSettlements(): Effect.Effect<void, unknown, never> {
    const self = this
    return Effect.gen(function* () {
      for (const promise of [...self.pendingSettlements]) {
        const exit = yield* self.observePromise(promise)
        if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause)) continue
        const failure = normalizeError(Cause.squash(exit.cause))
        throw new InterpreterRuntimeError(
          `Unhandled rejection from an un-awaited tool call: ${failure.message}`,
          undefined,
          failure.kind,
          ["Await tool calls - `const result = await tools.ns.tool(...)` - so failures can be caught and handled."],
        )
      }
    })
  }

  // Eagerly starts a tool call on a supervised child fiber (so the execution timeout and
  // scope teardown interrupt it) gated by the concurrency semaphore, and wraps the fiber in a
  // first-class promise value. `startImmediately` makes the runtime admit the call - charging
  // the tool-call budget and firing onToolCallStart - at the call site, before any await.
  private createToolCallPromise(
    path: ReadonlyArray<string>,
    args: Array<unknown>,
  ): Effect.Effect<SandboxPromise, never, R> {
    const self = this

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Await every tool call: const result = await tools.ns.tool(...) and wrap in try/catch if failure is expected
  2. If fire-and-forget is intended, ensure the call cannot fail or attach a catch handler supported by the runtime
  3. Check the failure.kind in the error to identify which tool failed and fix its inputs

Example fix

// before
tools.http.get(url); // un-awaited; failure surfaces at end of run
// after
const result = await tools.http.get(url);
Defensive patterns

Strategy: try-catch

Validate before calling

// Prefer awaiting every tool call; flag un-awaited calls statically
for (const m of code.matchAll(/(^|[^.\w])tools\.\w+\.\w+\s*\(/g)) {
  const prefix = code.slice(Math.max(0, m.index - 12), m.index + 12);
  if (!/await\s*$/.test(code.slice(0, m.index).trimEnd()) && !/\bawait\b/.test(prefix)) {
    console.warn('possible un-awaited tool call:', prefix.trim());
  }
}

Type guard

function isAwaitedToolCall(stmt) { return /await\s+tools\./.test(stmt); }

Try / catch

try {
  result = yield* interpreter.run(code);
} catch (e) {
  if (String(e.message).startsWith('Unhandled rejection from an un-awaited tool call')) {
    // identify the failing tool from failure.kind and add await + try/catch in code
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/4e6e53769d60a354. Report an issue: GitHub.