Skip to content

Code Mode execute: function expression (async () => {...}) silently returns null #57484

Open

Description

@lehcode

Description

Code Mode execute() receives a function expression (arrow or named) as the code argument — e.g. async () => { await tools.engram.mem_list_projects(); return "done" } — and always returns null. No errors are logged; the sandboxed runtime never invokes the function.

The runtime expects a function body (top-level statements with an explicit return). When the final expression is a function object itself, the JS engine returns that object; copyOut(undefinedAsNull) converts it to null. The body form works in every session tested.

Evidence:

  • 177 sessions scanned (opencode.db): 816 execute calls, 71 produced null.
  • 100% of null outputs share the same code shape: a top-level function expression (async () => ..., async (tools) => ..., or IIFE).
  • Zero nulls from body-form code across all sessions (816 total calls).
  • In-session surgical proof (seq 36–1347 body = all pass; seq 1504–1551 function expression = all null).
  • Live proof: async () => { return 42 } → null; const f = async () => 42; return f → null; (async () => 42)() (invoked IIFE) → 42.

Steps to reproduce

  1. Start an opencode session.
  2. In a Code Mode block, pass a function expression to execute:
async () => { return 42 }
  1. Observe the output: null.

Expected: 42.

OpenCode version

2.1.1

Operating system

Linux 7.2.8 (x86_64)

Additional context

  • Related: mcp: local tool calls return result: null, response text goes to Logs instead #48910 — same symptom (null from execute), but different root cause (missing return → undefined → null via undefinedAsNull). Our bug is the code value itself being a function expression.
  • The workaround (documented in AGENTS.md): always use body form with explicit return; to run a function, invoke it: (async () => { ... })().
  • This is a silent failure: no error, no warning, no log entry — the agent receives null and may think the call succeeded, while TUI confirms that tool failed.

Activity

  1. opencode-agent commented on Oct 20, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed write-up and the numbers from your sessions!

    As far as I can tell, Code Mode is doing what it was built to do here. execute treats code as a program body, and the tool description says: "Prefer an explicit return; if omitted, the final top-level expression becomes the result." With async () => { return 42 }, the final expression is the function object itself. It is never called, because nothing in the code calls it. The result is then converted the way JSON.stringify would convert it, which drops functions, so you get null. That's also why (async () => 42)() works: there the function is actually called.

    I agree that a silent null is easy to misread, especially for a model that wraps its code in an arrow function. So I'm treating this as a request to improve that: for example, a clear warning or error when the result is a function (something like "did you mean to call it?"), or calling a function result automatically. Maintainers will consider it, but I can't promise anything.

    Related issues:

    Until then, your workaround is the right one: write the code as a body with an explicit return, and if you do wrap it in a function, call it, e.g. return await (async () => { ... })().

  2. Dante-dan commented on Oct 20, 2026

    @Dante-dan
    Contributor

    I traced this on current dev (552f8671): the final expression is passed through toBoundary, which drops function values, and execution then normalizes undefined to null. The current path is the Acorn interpreter, rather than copyOut.

    I propose reporting a clear diagnostic only when the final program result is a function, with a hint such as “The program returned a function. Invoke it to run its body, or return a JSON value.” This would preserve ordinary undefined → null and nested JSON serialization behavior. Automatically calling the function could introduce unexpected tool calls or other side effects. This is separate from the parser-error enrichment discussed in #50577.

    Would maintainers approve that bounded design? I can implement it with coverage for arrow/named function results, body-form returns, and explicitly invoked functions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions