Description
@slack/socket-mode@2.0.7 can terminate the Node process with ERR_UNHANDLED_REJECTION when a Socket Mode reconnect is stopped while it is still pending.
The rejection reason is undefined, matching the argument emitted by State.Disconnected during shutdown.
Reproducible in
@slack/socket-mode: 2.0.7 (latest)
@slack/bolt: 4.7.2
- Node: 25.8.2 (also reproducible on 26.5.0)
- macOS 15 / arm64
Relevant code path
In SocketModeClient:
- The
close listener calls this.delayReconnectAttempt(this.start) without observing the returned Promise.
delayReconnectAttempt() calls cb.apply(this).then(res) with no rejection handler.
- If
disconnect() races that reconnect, the reconnecting start() rejects from State.Disconnected with undefined.
- The Promise returned by
.then(res) is unhandled, so modern Node terminates the process.
Minimal reproduction
This uses the public EventEmitter surface and replaces only the network attempt so the rejection path is deterministic:
const { SocketModeClient } = require('@slack/socket-mode');
const client = new SocketModeClient({
appToken: 'xapp-test',
autoReconnectEnabled: true,
clientPingTimeout: 1,
});
client.start = () => Promise.reject(undefined);
process.once('unhandledRejection', (reason) => {
console.log({ reproduced: true, reasonIsUndefined: reason === undefined });
process.exit(0);
});
client.emit('close');
setTimeout(() => process.exit(2), 100);
Output:
{ reproduced: true, reasonIsUndefined: true }
Production trigger observed
During a service restart, several Socket Mode connections opened but did not receive pong/hello before the client timeout. The application stopped the pending receiver after its startup deadline. The reconnect rejection then terminated the process; a supervisor restarted it and repeated the cycle until Slack connectivity recovered.
The characteristic log was:
WebSocket was closed before the connection was established
UnhandledPromiseRejection: ... reason "undefined"
code: 'ERR_UNHANDLED_REJECTION'
Expected behavior
Every reconnect attempt should have an observed rejection. Stopping during reconnect should settle/cancel the delayed reconnect without an unhandled Promise.
Suggested fix
Two changes appear sufficient:
void this.delayReconnectAttempt(this.start).catch((error) => {
if (!this.shuttingDown) this.logger.error(`Failed to reconnect Socket Mode client: ${error}`);
});
and propagate callback rejection from the delayed Promise:
return new Promise((resolve, reject) => {
setTimeout(() => {
if (this.shuttingDown) return resolve();
this.emit(State.Reconnecting);
cb.apply(this).then(resolve, reject);
}, msBeforeRetry);
});
I searched existing issues and found related reconnect reports (#1243, #2094), but not this current 2.0.7 dropped-rejection path.
Description
@slack/socket-mode@2.0.7can terminate the Node process withERR_UNHANDLED_REJECTIONwhen a Socket Mode reconnect is stopped while it is still pending.The rejection reason is
undefined, matching the argument emitted byState.Disconnectedduring shutdown.Reproducible in
@slack/socket-mode: 2.0.7 (latest)@slack/bolt: 4.7.2Relevant code path
In
SocketModeClient:closelistener callsthis.delayReconnectAttempt(this.start)without observing the returned Promise.delayReconnectAttempt()callscb.apply(this).then(res)with no rejection handler.disconnect()races that reconnect, the reconnectingstart()rejects fromState.Disconnectedwithundefined..then(res)is unhandled, so modern Node terminates the process.Minimal reproduction
This uses the public EventEmitter surface and replaces only the network attempt so the rejection path is deterministic:
Output:
Production trigger observed
During a service restart, several Socket Mode connections opened but did not receive pong/hello before the client timeout. The application stopped the pending receiver after its startup deadline. The reconnect rejection then terminated the process; a supervisor restarted it and repeated the cycle until Slack connectivity recovered.
The characteristic log was:
Expected behavior
Every reconnect attempt should have an observed rejection. Stopping during reconnect should settle/cancel the delayed reconnect without an unhandled Promise.
Suggested fix
Two changes appear sufficient:
and propagate callback rejection from the delayed Promise:
I searched existing issues and found related reconnect reports (#1243, #2094), but not this current 2.0.7 dropped-rejection path.