@vercel/postgres
^0.10.0postconditions16functions9last verified2026-06-24coverage score71%Postconditions: what we check
- sql · sql-query-no-error-handlingerrorWhenSQL query fails due to syntax error, connection issue, or constraint violationThrows
Database error with error code and messageRequired handlingWrap sql queries in try-catch blocks to handle database errors appropriatelycostmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[1] - sql · sql-incorrect-template-callerrorWhensql() is called as a regular function with a string argument instead of as a tagged template literal. Example: sql('SELECT * FROM users') instead of sql`SELECT * FROM users`.Throws
VercelPostgresError with code 'incorrect_tagged_template_call': "It looks like you tried to call `sql` as a function. Make sure to use it as a tagged template.\n\tExample: sql`SELECT * FROM users`, not sql('SELECT * FROM users')"Required handlingAlways use sql as a tagged template literal: const result = await sql`SELECT * FROM users WHERE id = ${userId}`; NOT as a function call: const result = await sql('SELECT * FROM users WHERE id = ' + userId); // THROWS The eslint-plugin-sql (or eslint-plugin-no-unsafe-queries) can catch this pattern at lint time. For dynamic queries, use pool.query() or client.query().costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[2] - sql · sql-missing-connection-string-on-first-useerrorWhenThe global sql template (or db alias) is used for the first time and the POSTGRES_URL environment variable is not set. The sql global lazily calls createPool() on first use — the error is deferred from initialization time to first query time, making it easy to miss in development.Throws
VercelPostgresError with code 'missing_connection_string': "You did not supply a 'connectionString' and no 'POSTGRES_URL' env var was found."Required handlingValidate POSTGRES_URL at app startup before relying on the global sql tag: if (!process.env.POSTGRES_URL) { throw new Error('POSTGRES_URL is required but not set'); } Or use createPool() explicitly with error handling: const pool = createPool(); // throws if POSTGRES_URL is missing try { const { rows } = await pool.sql`SELECT * FROM users`; } catch (error) { if (error instanceof VercelPostgresError && error.code === 'missing_connection_string') { console.error('Database not configured:', error.message); } throw error; } In Vercel deployments, POSTGRES_URL is injected automatically. In local development, add it to .env.local. In CI, add it to environment secrets.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[3] - query · query-no-error-handlingerrorWhenQuery fails due to connection issue or SQL errorThrows
Database errorRequired handlingWrap query() calls in try-catch blocks to handle connection and SQL errorscostmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[1] - connect · connect-no-cleanuperrorWhenClient obtained from pool is not releasedThrows
Connection pool exhaustion on subsequent queriesRequired handlingCall client.release() in a finally block to ensure connection is returned to poolcostmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - connect · pool-connect-callback-form-no-done-callerrorWhenpool.connect() is called with a callback argument (legacy form): pool.connect((err, client, done) => {...}) and the callback body does not call done() on all exit paths (including the error path and any early returns). Unlike the Promise form (where the returned client has a .release() method), the callback form provides done() as the ONLY release mechanism — failing to call it leaks the connection for the lifetime of the pool.Throws
No exception is thrown. The callback returns normally; the client is checked out indefinitely. Subsequent pool.connect() calls eventually stall waiting for a free client when max connections is reached. On Vercel serverless this manifests as request timeouts (504s), not stack-traceable errors.Required handlingPrefer the Promise form over the callback form: // ✅ PROMISE FORM — release() is on the returned client const client = await pool.connect(); try { const { rows } = await client.query('SELECT 1'); return rows; } finally { client.release(); } If you MUST use the callback form (e.g. integrating with legacy callback-driven middleware), call done() in every exit path: // ✅ CALLBACK FORM — done() in every branch pool.connect((err, client, done) => { if (err) { done(); // MUST be called even on connect error return reject(err); } client.query('SELECT 1', (queryErr, result) => { done(queryErr); // done(err) releases AND destroys on error if (queryErr) return reject(queryErr); resolve(result.rows); }); }); // ❌ MISSING done() ON ERROR — silent connection leak pool.connect((err, client, done) => { if (err) return reject(err); // leaked: done() never called client.query('SELECT 1', (queryErr, result) => { if (queryErr) return reject(queryErr); // leaked again done(); resolve(result.rows); }); });costhighin prodsilent failureusers seeservice unavailablevisibilitysilent - VercelPool · vercelpool-direct-constructor-skips-validationwarningWhen`new VercelPool(config)` is invoked directly instead of via the createPool() factory. The constructor accepts the config without checking that `config.connectionString` is present, non-empty, or of the pooled (`-pooler.`) form. An empty or wrong-type connection string is silently stored; the first query then fails with a confusing low-level error from @neondatabase/serverless rather than the clear VercelPostgresError that createPool() would throw at construction time.Throws
No exception at construction. First query produces an opaque connection error from the underlying neon driver, often presenting as "fetch failed" or "ECONNREFUSED" rather than the descriptive VercelPostgresError('missing_connection_string') / ('invalid_connection_string') that createPool() raises.Required handlingPrefer createPool() over direct construction in application code: // ✅ Factory — validates connection string + applies EdgeRuntime overrides const pool = createPool({ connectionString: process.env.POSTGRES_URL }); // ❌ Direct constructor — skips validation const pool = new VercelPool({ connectionString: process.env.POSTGRES_URL }); // If POSTGRES_URL is undefined, this constructs a dead pool that // fails opaquely on first query. If direct construction is required (e.g. for testing), validate the connection string yourself before instantiating: if (!config.connectionString) { throw new Error('VercelPool requires a connectionString'); } if (!config.connectionString.includes('-pooler.')) { throw new Error('VercelPool requires a pooled connection string'); } const pool = new VercelPool(config);costmediumin proddelayed failureusers seeservice unavailablevisibilitysilentSources[7] - createPool · createpool-missing-connection-stringerrorWhenPOSTGRES_URL environment variable is not set (undefined or the literal string "undefined") and no connectionString is passed in config.Throws
VercelPostgresError with code 'missing_connection_string': "You did not supply a 'connectionString' and no 'POSTGRES_URL' env var was found."Required handlingCaller MUST ensure POSTGRES_URL is set in the environment before calling createPool(). In Vercel deployments this is injected automatically; in local development it must be in .env.local. Missing connection string causes VercelPostgresError at call time — it will not throw at query time. Pattern: call createPool() inside a try-catch at module initialization, or validate env vars at app startup before any database access.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[8] - createPool · createpool-wrong-connection-string-typeerrorWhenA direct (non-pooled) connection string is passed to createPool(). The direct connection string uses the standard host format; pooled strings contain '-pooler.' in the hostname. Using a direct connection string with createPool() triggers an explicit validation error.Throws
VercelPostgresError with code 'invalid_connection_string': "This connection string is meant to be used with a direct connection. Make sure to use a pooled connection string or try createClient() instead."Required handlingUse POSTGRES_URL (pooled) with createPool() and POSTGRES_URL_NON_POOLING (direct) with createClient(). Mixing them causes VercelPostgresError at call time. This is a common mistake when manually setting connection strings.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[8] - createClient · createclient-missing-connection-stringerrorWhenPOSTGRES_URL_NON_POOLING environment variable is not set and no connectionString is passed in config.Throws
VercelPostgresError with code 'missing_connection_string': "You did not supply a 'connectionString' and no 'POSTGRES_URL_NON_POOLING' env var was found."Required handlingEnsure POSTGRES_URL_NON_POOLING is set before calling createClient(). In Vercel deployments, both POSTGRES_URL and POSTGRES_URL_NON_POOLING are injected automatically. In local development, both must be in .env.local.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[8] - createClient · createclient-pooled-connection-stringerrorWhenA pooled connection string (containing '-pooler.' in hostname) is passed to createClient() instead of a direct connection string.Throws
VercelPostgresError with code 'invalid_connection_string': "This connection string is meant to be used with a pooled connection. Try createPool() instead."Required handlingUse POSTGRES_URL_NON_POOLING (direct connection) with createClient(). Use POSTGRES_URL (pooled) with createPool() or the global sql/db template. In serverless environments, prefer createPool() (or the global sql tag) over createClient() to benefit from connection pooling.costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[8] - createClient · createclient-query-before-connecterrorWhenclient.query() or client.sql`` is called on a VercelClient before calling client.connect().Throws
Error: query is called on a client that is not connected. The underlying pg Client rejects with a connection error.Required handlingAlways call await client.connect() before issuing any queries on a VercelClient. Wrap the entire client lifecycle in try-catch-finally: const client = createClient(); try { await client.connect(); const result = await client.sql`SELECT * FROM users`; return result.rows; } catch (error) { console.error('Database error:', error); throw error; } finally { await client.end(); // always release the connection }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[9] - pool.end · pool-end-no-error-handlingwarningWhenpool.end() is called but not awaited, or the Promise is not handled. Clients may not be fully released before the process exits.Throws
Pool.end() returns a Promise<void>. If not awaited, the shutdown is fire-and-forget and connections may not close cleanly before process exit.Required handlingAlways await pool.end() in shutdown handlers: process.on('SIGTERM', async () => { try { await pool.end(); } catch (error) { console.error('Error closing pool:', error); } finally { process.exit(0); } });costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - pool.end · pool-end-queries-after-shutdownerrorWhenAny query (sql`...`, pool.query(), pool.connect()) is attempted after pool.end() has been called.Throws
Error: pool has been shut down — the pool refuses new client checkouts after end() is called.Required handlingDo not issue queries after calling pool.end(). In serverless environments (Vercel Edge/Lambda), pool lifecycle is managed per-invocation — do not call pool.end() in the middle of request handling.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[5] - client.end · client-end-not-callederrorWhencreateClient() is used to run queries but client.end() is never called, or is called in a code path that can be skipped when an error occurs (not in a finally block).Throws
No immediate exception — connection leak is silent. The PostgreSQL server will eventually close idle connections based on idle_in_transaction_session_timeout and tcp_keepalives settings. In the meantime, each invocation creates a new connection that is never returned.Required handlingALWAYS call client.end() in a finally block to guarantee connection cleanup regardless of query success or failure: const client = createClient(); try { await client.connect(); const { rows } = await client.query('SELECT * FROM users'); return rows; } catch (error) { console.error('Query failed:', error); throw error; } finally { await client.end(); // MUST be in finally — not try } Failure pattern (DO NOT DO THIS): const client = createClient(); await client.connect(); const { rows } = await client.query('...'); // if this throws, end() is never called await client.end(); // unreachable on errorcosthighin prodsilent failureusers seeservice unavailablevisibilitysilent - postgresConnectionString · postgresconnectionstring-invalid-typewarningWhenAn invalid type string (anything other than 'pool' or 'direct') is passed to postgresConnectionString(). This is a programming error — the TypeScript type system prevents it at compile time, but JavaScript callers or poorly typed code can still trigger it.Throws
VercelPostgresError with code 'invalid_connection_type': "Unhandled type: <type>"Required handlingAlways pass 'pool' or 'direct'. For type-safe callers this is enforced at compile time. For JavaScript or dynamically-typed code, validate the type argument first: const type = getUserInput(); // 'pool' | 'direct' | unknown if (type !== 'pool' && type !== 'direct') { throw new Error(`Invalid connection type: ${type}`); } const connectionString = postgresConnectionString(type);costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[11]
Sources
Every postcondition cites at least one of these. Grouped by source type; numbered to match the footnotes above.
- [1]vercel.com/docs/storage/vercel-postgresVercel Postgres
- [4]vercel.com/kb/guide/connection-pooling-with-functionsConnection Pooling With Functions
- [5]node-postgres.com/features/poolingPooling
- [6]node-postgres.com/apis/poolPool
- [9]node-postgres.com/apis/clientClient
- [10]node-postgres.com/features/transactionsTransactions
- [2]github.com/vercel/storage/blobvercel/storage · sql-template.ts
- [3]github.com/vercel/storage/blobvercel/storage · index.ts
- [7]github.com/vercel/storage/blobvercel/storage · create-pool.ts
- [8]github.com/vercel/storage/treevercel/storage · postgres
- [11]github.com/vercel/storage/blobvercel/storage · postgres-connection-string.ts
Research notes
Curator notes from SOURCES.md captured when the profile was written so you can verify the reasoning, not just the rules.
Sources: @vercel/postgres
Package: @vercel/postgres
Version: 0.10.0
Category: database (Serverless Postgres client)
Status: ⚠️ Deprecated - Migrated to Neon
Official Documentation
- Main Docs: https://vercel.com/docs/storage/vercel-postgres
- Error Codes: https://vercel.com/docs/storage/vercel-postgres/vercel-postgres-error-codes
- Connection Pooling: https://vercel.com/kb/guide/connection-pooling-with-functions
- npm: https://www.npmjs.com/package/@vercel/postgres
- Repository: https://github.com/vercel/storage
Behavioral Requirements
SQL Query Errors: Syntax errors, constraint violations, type mismatches Connection Errors: Pool exhaustion, missing connection string Must wrap sql/query in try-catch for error handling Must release pooled connections in finally blocks
Contract Rationale
Critical for serverless: Limited connection pools (5-20), high concurrency, stateless functions
SQL errors crash functions → 500 responses
Connection leaks exhaust pool → application hangs
Created: 2026-02-25 Status: ✅ COMPLETE