Skip to content

Let execute return { error } when throwOnError is false - #566

Open
andrelandgraf wants to merge 4 commits into
mainfrom
tools-t6-throw-on-error
Open

andrelandgraf wants to merge 4 commits into
mainfrom
tools-t6-throw-on-error

Conversation

@andrelandgraf

@andrelandgraf andrelandgraf commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Problem

Every @neon/tools tool throws NeonError when the Neon API returns an error. Agent frameworks usually want the failure back as a tool result the model can read, and callers who want to branch on the error have to wrap every execute in try/catch. There was no way to get the error as a typed value.

What changed

createNeonTools and createNeonTool take throwOnError?: boolean. Omitted or true is the current behavior: execute returns { data } and throws NeonError on API failures. With throwOnError: false, execute catches NeonError and returns it as { error }, and the result type becomes { data } | { error }. Zod input-validation failures and non-Neon exceptions still throw in both modes.

The envelope wraps outside onExecute, so an onExecute interceptor still observes the thrown NeonError before it becomes a value.

Interface

import { createNeonTools } from "@neon/tools";

const tools = createNeonTools({
	apiKey,
	tools: ["projects.get"] as const,
	throwOnError: false,
});

const { data, error } = await tools["projects.get"].execute({
	project_id: "project-id",
});
if (error) {
	// error: NeonError
} else {
	// data.id: string
}

The { data, error } destructure works because the literal throwOnError: false narrows the result to the envelope union. When the options object is annotated as CreateNeonToolsOptions, throwOnError stays optional and execute types both outcomes; narrow with "error" in result:

const options: CreateNeonToolsOptions<["projects.get"]> = {
	apiKey,
	tools: ["projects.get"],
	throwOnError: false,
};

const result = await createNeonTools(options)["projects.get"].execute({
	project_id: "project-id",
});
if ("error" in result && result.error) {
	// NeonError
}

Adapters

registerNeonTools (both @neon/tools/mcp and @neon/tools/mcp-v1), toEveTool and toMastraTools accept envelope-typed tools. The MCP handler re-throws a returned { error }, so an enveloped tool produces the same isError MCP result as a throwing one.

NeonExecutableTool is a new export: the execute-capable tool type whose result covers both throw modes. The adapters type their inputs with it, replacing the private per-adapter execute shapes.

Nothing changes for existing callers. throwOnError omitted keeps execute typed as Promise<{ data }> and throwing.

Also in here

  • throwOnError is on ToolClientOptions in the generated-tools binding, so NeonToolsClientOptions carries the flag.
  • Test helpers that spread a NeonToolsClientOptions value into createNeonTools now take Omit<NeonToolsClientOptions, "throwOnError">; spreading a boolean-typed flag would erase the literal narrowing the tests assert on.
  • Changeset: @neon/tools patch.
  • README documents the flag with the destructure example.

Verification

pnpm --filter @neon/tools exec vitest run --typecheck at 398f876: 12 files, 145 tests, type tests included, all green. Behaviors covered:

  • a 404 with throwOnError: false returns { error } with error instanceof NeonError and kind: "not_found"
  • same envelope from a single createNeonTool
  • Zod input failures still throw with throwOnError: false
  • onExecute sees the thrown NeonError and the caller still gets { error }
  • omitted throwOnError still rejects with NeonError
  • type-level: literal false narrows to the envelope, an annotated CreateNeonToolsOptions types both outcomes, and enveloped tools pass registerNeonTools, toEveTool and toMastraTools

The suite runs against an injected fetch returning canned responses; no live API run.

For your attention

  • The envelope covers NeonError only. Network faults from a custom fetch and other exceptions still throw with throwOnError: false, so a { error } result always means a Neon API error.
  • WithThrowMode rewrites execute by inferring the Promise<{ data }> shape from the tool type; a tool whose execute does not match that shape passes through untouched.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant