From 6f0f0e89ba764f2e69a4efd951a8d9b112c8e2d0 Mon Sep 17 00:00:00 2001 From: Chris Bala Date: Tue, 29 Sep 2026 19:43:41 -0700 Subject: [PATCH] Return the cancel request's promise from query.cancel() `query.cancel()` sent the cancel request on a connection of its own but dropped the promise that request settles, and returned null. A cancel request that failed, for example because that connection could not be opened, was then an unhandled rejection, which ends a Node process by default, and the caller had no way to handle it. Return the promise instead, and the same promise on later calls, which still send no second request. Its type is now `Promise`, and the README says how it settles. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 2 ++ src/query.js | 2 +- tests/index.js | 25 +++++++++++++++++++++++++ types/index.d.ts | 6 +++++- 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3e73281..f353bb8 100644 --- a/README.md +++ b/README.md @@ -570,6 +570,8 @@ setTimeout(() => query.cancel(), 100) const result = await query ``` +`query.cancel()` returns a promise that settles when the cancel request is done. It rejects if the request could not be sent, for example when a connection for it cannot be opened, so handle it to avoid an unhandled rejection. Calling it again returns the same promise. + ### Execute #### ```await sql``.execute()``` diff --git a/src/query.js b/src/query.js index 0d44a15..0227cc8 100644 --- a/src/query.js +++ b/src/query.js @@ -50,7 +50,7 @@ export class Query extends Promise { } cancel() { - return this.canceller && (this.canceller(this), this.canceller = null) + return this.cancelling || (this.cancelling = this.canceller && this.canceller(this)) } simple() { diff --git a/tests/index.js b/tests/index.js index 845c6ce..d7f921e 100644 --- a/tests/index.js +++ b/tests/index.js @@ -2302,6 +2302,31 @@ t('Cancel queued query', async() => { return ['57014', error.code] }) +t('Cancel returns a promise that rejects when the cancel request fails', async() => { + let sockets = 0 + const sql = postgres({ + ...options, + socket: () => { + if (sockets++) + throw new Error('no socket for the cancel request') + return new Promise((resolve, reject) => { + const socket = new net.Socket() + socket.connect(5432) + socket.on('error', reject) + socket.on('connect', () => resolve(socket)) + }) + } + }) + await sql`select 1` + const query = sql`select pg_sleep(0.2)`.execute() + await delay(50) + const cancelled = query.cancel() + const rejected = await cancelled.then(() => false, e => e instanceof Error) + await query + await sql.end() + return ['true,true', [rejected, query.cancel() === cancelled].join()] +}) + t('Fragments', async() => [ 1, (await sql` diff --git a/types/index.d.ts b/types/index.d.ts index 13c3432..6103ccd 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -605,7 +605,11 @@ declare namespace postgres { writable(): Promise; execute(): this; - cancel(): void; + /** + * Sends a cancel request for this query, once, however often it is called + * @returns A promise that settles when the cancel request is done, and rejects if it could not be sent + */ + cancel(): Promise; /** * @deprecated `.stream` has been renamed to `.forEach`