Environment details
- OS: macOS (Darwin 25.3.0) / also reproducible on Linux (Cloud Run)
- Node.js version: 24.14.1
@google-cloud/spanner version: 8.6.0
@grpc/grpc-js version: 1.14.3
Steps to reproduce
// repro.mjs — run with: node repro.mjs
import { Spanner } from "@google-cloud/spanner";
import { credentials } from "@grpc/grpc-js";
const spanner = new Spanner({
projectId: "test-project",
servicePath: "localhost",
port: 15000,
sslCreds: credentials.createInsecure(),
});
console.log("calling await spanner.close()...");
await spanner.close();
console.log("this line is never reached");
$ node repro.mjs
calling await spanner.close()...
Warning: Detected unsettled top-level await at file:///repro.mjs:12
await spanner.close();
^
$ echo $?
13
No Spanner instance is required — the bug is in the close() method itself.
Expected behavior
await spanner.close() should resolve once cleanup is complete.
Actual behavior
The Promise returned by spanner.close() never resolves. On Node 24, this causes the process to exit with code 13 ("unsettled top-level await"). On earlier Node versions, the process hangs until forcefully terminated.
Root cause
Spanner.close() is wrapped by promisifyAll() at the bottom of src/index.ts, and close is not in the exclude list:
https://github.com/googleapis/google-cloud-node/blob/main/packages/handwritten/src/spanner/src/index.ts#L2114-L2128
When called without a callback argument, the promisify wrapper creates a Promise and passes a synthetic callback to the original method, expecting it to be invoked. However, Spanner.close() ignores all arguments:
https://github.com/googleapis/google-cloud-node/blob/main/packages/handwritten/src/spanner/src/index.ts#L543-L554
close() {
this.clients_.forEach(c => {
const client = c as GapicClient;
if (client.operationsClient && client.operationsClient.close) {
client.operationsClient.close();
}
client.close();
});
cleanup().catch(err => {
console.error('Error occured during cleanup: ', err);
});
// ← callback is never called
}
Since the callback is never invoked, the Promise stays pending forever.
The TypeScript declaration (close(): void) correctly reflects the implementation, but at runtime the promisify wrapper overrides the return type.
A similar issue was reported for Database.close() in googleapis/nodejs-spanner#1828.
Suggested fix
Either:
- Add
'close' to the promisifyAll exclude list (if close() is intended to remain fire-and-forget), or
- Accept and invoke the callback parameter:
close(callback?: (err: Error | null) => void) {
this.clients_.forEach(c => {
const client = c as GapicClient;
if (client.operationsClient && client.operationsClient.close) {
client.operationsClient.close();
}
client.close();
});
cleanup()
.then(() => callback?.(null))
.catch(err => callback?.(err));
}
Environment details
@google-cloud/spannerversion: 8.6.0@grpc/grpc-jsversion: 1.14.3Steps to reproduce
No Spanner instance is required — the bug is in the
close()method itself.Expected behavior
await spanner.close()should resolve once cleanup is complete.Actual behavior
The Promise returned by
spanner.close()never resolves. On Node 24, this causes the process to exit with code 13 ("unsettled top-level await"). On earlier Node versions, the process hangs until forcefully terminated.Root cause
Spanner.close()is wrapped bypromisifyAll()at the bottom ofsrc/index.ts, andcloseis not in theexcludelist:https://github.com/googleapis/google-cloud-node/blob/main/packages/handwritten/src/spanner/src/index.ts#L2114-L2128
When called without a callback argument, the promisify wrapper creates a Promise and passes a synthetic callback to the original method, expecting it to be invoked. However,
Spanner.close()ignores all arguments:https://github.com/googleapis/google-cloud-node/blob/main/packages/handwritten/src/spanner/src/index.ts#L543-L554
Since the callback is never invoked, the Promise stays pending forever.
The TypeScript declaration (
close(): void) correctly reflects the implementation, but at runtime the promisify wrapper overrides the return type.A similar issue was reported for
Database.close()in googleapis/nodejs-spanner#1828.Suggested fix
Either:
'close'to thepromisifyAllexclude list (ifclose()is intended to remain fire-and-forget), or