Skip to content

Error Handling

All errors thrown by QueryMode are QueryModeError instances with a structured code field. Low-level errors (ENOENT, parse failures, OOM) are wrapped automatically with context.

CodeWhenExample message
TABLE_NOT_FOUNDFile doesn’t exist, R2 key missing”Table not found: events.lance”
COLUMN_NOT_FOUNDColumn name not in schema”Column not found: foo”
INVALID_FORMATFile can’t be parsed, decompression fails, bad binary format”Invalid table format: data.xyz. Supported formats: .lance, .parquet, .csv, …”
SCHEMA_MISMATCHColumn type doesn’t match operation, unsupported dtype”Column not found in events: age”
INVALID_FILTERBad filter op or value type”Invalid filter: unknown op ‘regex‘“
INVALID_AGGREGATEBad aggregate function or missing column”Invalid aggregate: sum requires numeric column”
MEMORY_EXCEEDEDOperator exceeds memory budget”Memory budget exceeded querying events”
NETWORK_TIMEOUTR2 or RPC call timed out”Network timeout on events: R2 read timed out”
QUERY_TIMEOUTTotal query time exceeded”Query timeout on events”
QUERY_FAILEDCatch-all for unclassified errors”Query failed on events: …”
import { QueryModeError } from "querymode"
try {
await qm.table("missing.lance").collect()
} catch (err) {
if (err instanceof QueryModeError) {
switch (err.code) {
case "TABLE_NOT_FOUND":
console.log("Table doesn't exist:", err.message)
break
case "MEMORY_EXCEEDED":
console.log("Try adding filters or reducing projections")
break
default:
console.log(`${err.code}: ${err.message}`)
}
// Original error is preserved
if (err.cause) console.log("Caused by:", err.cause)
}
}

QueryModeError.from() wraps any error with context:

try {
await riskyOperation()
} catch (err) {
throw QueryModeError.from(err, { table: "events", operation: "scan" })
// Automatically classifies: ENOENT → TABLE_NOT_FOUND,
// "footer" in message → INVALID_FORMAT, "OOM" → MEMORY_EXCEEDED, etc.
}

Already-wrapped QueryModeError instances pass through unchanged.

When using the HTTP API (/query, /write, etc.), error codes map to HTTP status codes:

Error codeHTTP statusMeaning
TABLE_NOT_FOUND404Table doesn’t exist in R2
COLUMN_NOT_FOUND404Column not in schema
INVALID_FILTER400Malformed filter op or value
INVALID_FORMAT400File can’t be parsed
INVALID_AGGREGATE400Bad aggregate function
SCHEMA_MISMATCH400Column type doesn’t match operation
MEMORY_EXCEEDED503Operator exceeded memory budget
QUERY_TIMEOUT504Total query time exceeded
NETWORK_TIMEOUT504R2 or RPC call timed out
QUERY_FAILED500Unclassified error
(SyntaxError)400Malformed JSON request body
(CAS conflict)409Concurrent write conflict

The response body always includes { error: string } and, for QueryModeError, { code: string }:

{ "error": "Column not found in events: foo", "code": "SCHEMA_MISMATCH" }

When connecting via the pg-wire protocol, errors include standard SQLSTATE codes (e.g., 42P01 for table not found, 57014 for timeout). See pg-wire error codes for the full mapping.

The DataFrame API validates arguments eagerly and throws QueryModeError with QUERY_FAILED:

qm.table("events").limit(-1)
// QueryModeError: QUERY_FAILED — "limit() must be non-negative"
qm.table("events").after(100)
// QueryModeError: QUERY_FAILED — "after() requires sort()"
await qm.table("events").append(rows)
// QueryModeError: QUERY_FAILED — "append() requires an executor with write support"
// (when using a read-only executor)

WASM out-of-memory errors throw MEMORY_EXCEEDED:

// QueryModeError: MEMORY_EXCEEDED — "WASM OOM allocating zstd output"

SQL syntax errors throw SqlParseError or SqlLexerError (not QueryModeError) with a position field indicating where the error occurred:

import { SqlParseError, SqlLexerError } from "querymode"
try {
await qm.sql("SELECT FROM").collect()
} catch (err) {
if (err instanceof SqlParseError) {
console.log(err.message) // "Expected column or expression at position 7"
console.log(err.position) // 7
}
if (err instanceof SqlLexerError) {
console.log(err.message) // "Unexpected character at position 12"
console.log(err.position) // 12
}
}