Skip to content

SocketModeClient reconnect rejection is unhandled when disconnect races retry #2656

Description

@totoday

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:

  1. The close listener calls this.delayReconnectAttempt(this.start) without observing the returned Promise.
  2. delayReconnectAttempt() calls cb.apply(this).then(res) with no rejection handler.
  3. If disconnect() races that reconnect, the reconnecting start() rejects from State.Disconnected with undefined.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugM-T: A confirmed bug report. Issues are confirmed when the reproduction steps are documentedpkg:socket-modeapplies to `@slack/socket-mode`

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions