Skip to content
Query.Farm
Talk with Us

Report errors well

Anything you throw becomes what the person running the query sees. Throwing the right thing is the difference between “argument ‘count’ must be >= 0” and a wall of stack frames.

All extend VgiError, and the class name travels to the client as the error type:

ClassThrow it when
ArgumentValidationErrorAn argument is out of range, the wrong shape, or fails a rule you enforce yourself.
RowCountMismatchErrorRaised for you: a scalar compute returned a different number of values than rows it was given.
FunctionNotFoundErrorA name the worker doesn’t serve. Mostly raised by the framework.
CatalogReadOnlyErrorA DML attempt against a read-only catalog.
CatalogNotFoundError / CatalogAlreadyExistsErrorCatalog, schema, table or view lookup and creation conflicts.
NoCatalogErrorThe worker was built with no catalog configured at all.

Anything else — a plain Error, a TypeError from your own code — still reaches the user, just carrying its own class name instead.

import { ArgumentValidationError } from "@query-farm/vgi";

onBind: (params) => {
if (!params.bindCall.input_schema) {
  throw new ArgumentValidationError("filter_positive requires a table argument");
}
return { outputSchema: params.bindCall.input_schema };
},

Before writing a validation by hand, check whether argConstraints already covers it. Declared constraints are enforced at bind, before any data moves, and produce a well-formed error without you writing one:

args: { count: int },
argConstraints: { count: { ge: 0 } },
Invalid Input Error: VGI Worker Exception: ArgumentValidationError: argument 'count' must be >= 0

ge, le, gt, lt, choices and pattern are all available, and they double as discovery metadata — vgi_function_arguments() reports them, so an agent inspecting the catalog learns the valid range without calling anything.

Errors ship their stack, not just their message

The wire format carries traceback: error.stack, and the extension prints it. A thrown error surfaces in the SQL client as the message plus the JS stack, with absolute paths from the worker machine:

Invalid Input Error: VGI Worker Exception: TypeError: Invalid mix of BigInt and other type in addition.
TypeError: Invalid mix of BigInt and other type in addition.
  at <anonymous> (/srv/workers/rates.ts:10:72)
  at dispatchStream (/srv/node_modules/@query-farm/vgi-rpc/src/dispatch/stream.ts:157:22)
  …

That is deliberate and it is excellent while developing. Two consequences worth planning for:

  • It leaks your filesystem layout and dependency versions to whoever can run a query. On a worker serving untrusted callers, catch at the boundary and rethrow a clean ArgumentValidationError with a message you chose.
  • The first line is the one users read. Put the actionable part in the message, not in a comment three frames down.

The message reaches someone at a SQL prompt who may not know a worker exists. Name the argument, say what was expected, and skip the internals:

// Unhelpful — true, and unactionable.
throw new Error("bad input");

// Better — names the argument, the rule, and the value that broke it.
throw new ArgumentValidationError(
`argument 'precision' must be between 0 and 10, got ${precision}`,
);
Where to put what isn't an error

console.log goes to stdout, which on the stdio transport is the protocol — never print there. Use console.error (stderr) for operator diagnostics, and out.clientLog(level, msg) to send a message in-band to the DuckDB client, where it surfaces in duckdb_logs() with type='VGI'. Use the in-band channel for something the user can act on — a row skipped, a credential about to expire.

onBind, outputType, process and the storage calls may all be async, and a rejected promise is reported exactly like a throw. What is not reported is a floating promise:

// The failure is invisible — the query succeeds with missing rows.
someAsyncWork();

// Awaited, so a rejection becomes the query's error.
await someAsyncWork();

A missing await inside process is the most common way a TypeScript worker returns quietly wrong results instead of failing.