runlot

env.db

The database API you use from a worker. It provides exec, tryExec, session, and identity.

export interface Db {
  exec(sql: string, params?: unknown[]): Promise<unknown[]>;
  tryExec(sql: string, params?: unknown[]): Promise<TryExecResult>;
  session(): Promise<DbSession>;
  identity(): Promise<{ project: string; epoch: number }>;
}

exec

Returns an array of rows on success, and throws if the SQL fails to run.

const rows = await env.db.exec(
  "insert into posts (title) values ($1) returning id",
  ["hello"],
);

Bind parameters are numbered from $1. Pass parameters as the second argument instead of concatenating them into the SQL string.

Each statement has a 10-second limit. Past that it is cancelled with 57014.

tryExec

Returns a result object instead of throwing. Use this API when your code needs to inspect the SQLSTATE reliably.

const r = await env.db.tryExec("insert into users (email) values ($1)", [email]);
if (!r.ok) {
  if (r.error.code === "23505") return new Response("That email is already registered", { status: 409 });
  throw new Error(r.error.message);
}

A successful result has this shape:

{
  ok: true,
  columns: [{ name: "id", typeOid: 23 }],
  values: [[1]],                  // positional array: values are preserved even when column names repeat
  rows: [{ id: 1 }],
  commandTag: "SELECT 1",
  transactionStatus: "I",
}

A failed result has this shape:

{
  ok: false,
  error: { code: "23505", message: "…", detail: null, hint: null, position: null, severity: "ERROR" },
}
The Error thrown by exec has no .code property, because custom properties are not preserved on error objects that cross the worker boundary. The SQLSTATE is prefixed to the error message as in [42P01], but the value your program should rely on is tryExec(...).error.code. With @runlot/pg the handling happens in JavaScript inside the worker, so err.code is available.

session

Use this when several SQL statements have to run in the same session, such as BEGIN … COMMIT.

const s = await env.db.session();
try {
  await s.exec("begin");
  await s.exec("update accounts set balance = balance - $1 where id = $2", [100, 1]);
  await s.exec("update accounts set balance = balance + $1 where id = $2", [100, 2]);
  await s.exec("commit");
} finally {
  await s.close();
}

close() is safe to call more than once, and it rolls back any open transaction. To commit, send COMMIT yourself rather than relying on close(). See Sessions for details.

identity

Returns the project the current request is attached to and the generation number of the database. Useful for debugging and logging.

const { project, epoch } = await env.db.identity();

How SQL values become JavaScript values

The rule is that no value is silently lost.

SQL typeJavaScript value
int2 int4 float4 float8number
int8 numericstring — never rounded into a number
boolboolean
text varchar uuidstring
byteaArrayBuffer
json jsonbthe parsed value, or the original string if parsing fails
timestamp timestamptz datestring — time zone and precision are preserved
arrays, domains, extension types{ text, typeOid } or string

Types that are not in the table are returned as strings. We never guess at an underlying type and convert it for you.

The timestamptz string is in PostgreSQL's notation (2026-09-04 13:38:14+00). Not every browser's new Date() parses that shape (Safari returns Invalid Date). If you need ISO 8601, wrap the column in to_json(col) in SQL, or use @runlot/pg.

@runlot/pg additionally applies node-postgres' default type parsers. With that package, timestamptz and date come back as Date objects. Keep in mind that the two APIs differ on date types.

On this page