Migrating @slack/bolt from v4 to v5
Minimum Node.js version: 20
Bolt for JS v5 follows the Node Slack SDK's shift from axios to the native Fetch API. It also removes the deprecated Workflow Steps feature (retired by Slack in September 2024) and raises the minimum Node.js version to 20.
All internal @slack/* Node Slack SDK dependencies have been bumped to their next major versions.
If your app doesn't use proxy/TLS configuration or inspect respond() utility function return values, this upgrade is likely a version bump and done.
Breaking Changes
We've raised the minimum Node.js version to 20
We've dropped support for Node.js 18. Node.js 20 or later is required. The minimum npm version has also been raised to 9.6.4.
TypeScript consumers target ES2022 or later
Bolt v5 depends on v8 of the @slack/web-api Node Slack SDK package, whose error classes use the ES2022 Error(message, { cause }) constructor. Its type definitions reference the global ErrorOptions type.
If your project's tsconfig.json targets an older ECMAScript library and does not set skipLibCheck, your build may fail after upgrading with an error like:
error TS2304: Cannot find name 'ErrorOptions'.
To fix it we recommend raising your compilation target to es2022 or later, as it aligns with the Node.js 20 baseline.
Alternatively, set "skipLibCheck": true to skip type-checking of dependency declaration files. Note that Bolt's own build and its sample apps already extend @tsconfig/node20, which targets es2022, so projects based on those templates are unaffected.
We've removed the agent and clientTls options from the AppOptions interface
You should configure transport via the clientOptions.fetch option or use the Node.js built-in proxy support.
Before (v4):
import { App } from '@slack/bolt';
import { HttpsProxyAgent } from 'https-proxy-agent';
import fs from 'node:fs';
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
agent: new HttpsProxyAgent('http://corporate.proxy:8080'),
clientTls: {
cert: fs.readFileSync('/path/to/client-cert.pem'),
key: fs.readFileSync('/path/to/client-key.pem'),
},
});
Preferred: Built-in proxy support
Node.js can read proxy environment variables natively via http.setGlobalProxyFromEnv(). Call it once at startup and globalThis.fetch routes through your proxy automatically, no extra packages needed.
Option A: programmatically call once at startup
import http from 'node:http';
import { App } from '@slack/bolt';
http.setGlobalProxyFromEnv();
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
});
Option B: use an environment variable
NODE_USE_ENV_PROXY=1 HTTPS_PROXY=http://corporate.proxy:8080 node app.js
import { App } from '@slack/bolt';
// No proxy configuration needed — globalThis.fetch respects the environment
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
});
Alternative: use an undici Dispatcher instance for proxy and TLS
If you need per-client configuration, use the clientOptions.fetch option with an undici Dispatcher instance:
import { App } from '@slack/bolt';
import { fetch, ProxyAgent, Agent } from 'undici';
import fs from 'node:fs';
// Proxy only
const proxyDispatcher = new ProxyAgent('http://corporate.proxy:8080');
// TLS only
const tlsDispatcher = new Agent({
connect: {
cert: fs.readFileSync('/path/to/client-cert.pem'),
key: fs.readFileSync('/path/to/client-key.pem'),
ca: fs.readFileSync('/path/to/ca-cert.pem'),
},
});
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
clientOptions: {
fetch: (url, init) => fetch(url, { ...init, dispatcher: tlsDispatcher }),
},
});
We've updated the SocketModeReceiver class to accept a dispatcher option instead of proxy agents
The SocketModeReceiver class now accepts a dispatcher option for unified proxy and TLS configuration of both the WebSocket connection and HTTP API calls. The dispatcher option accepts any undici-compatible Dispatcher instance.
Before (v4):
import { App } from '@slack/bolt';
import { HttpsProxyAgent } from 'https-proxy-agent';
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
appToken: process.env.SLACK_APP_TOKEN,
socketMode: true,
agent: new HttpsProxyAgent('http://corporate.proxy:8080'),
});
Preferred: Use built-in proxy support
Node.js can read proxy environment variables natively via http.setGlobalProxyFromEnv(). Call it once at startup and both the WebSocket connection and API calls route through your proxy automatically.
import http from 'node:http';
import { App } from '@slack/bolt';
http.setGlobalProxyFromEnv();
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
appToken: process.env.SLACK_APP_TOKEN,
socketMode: true,
});
Alternative: Use an undici Dispatcher instance for proxy and TLS
If you need per-client configuration, pass a dispatcher option to both the SocketModeReceiver class (for the WebSocket connection) and the clientOptions.fetch option (for the app's internal WebClient class API calls):
import { App, SocketModeReceiver } from '@slack/bolt';
import { fetch, ProxyAgent } from 'undici';
const dispatcher = new ProxyAgent('http://corporate.proxy:8080');
const receiver = new SocketModeReceiver({
appToken: process.env.SLACK_APP_TOKEN,
dispatcher,
});
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
receiver,
clientOptions: {
fetch: (url, init) => fetch(url, { ...init, dispatcher }),
},
});
We've removed Workflow Steps from Apps
The Workflow Steps from Apps feature was retired in September 2024. The WorkflowStep class, the app.step() method, and all related types have been deleted from Bolt for JS. You should remove any imports of the WorkflowStep class or the WorkflowStepEdit type.
Use the app.function() method with custom functions instead.
We've updated the respond() utility function to return a native Response object
The respond() utility function now uses native fetch internally. If you inspect the return value, the shape has changed from an AxiosResponse object to a standard Fetch Response object.
Before (v4):
app.command('/ticket', async ({ command, ack, respond }) => {
await ack();
const result = await respond(`Ticket created: ${command.text}`);
// result was an AxiosResponse
console.log(result.status); // 200
console.log(result.data); // response body (pre-parsed)
console.log(result.headers); // AxiosHeaders object
});
After (v5):
app.command('/ticket', async ({ command, ack, respond }) => {
await ack();
const result = await respond(`Ticket created: ${command.text}`);
// result is a native Fetch Response
console.log(result.status); // 200
console.log(await result.text()); // response body (call .text() or .json())
console.log(result.headers); // Headers object
});
In addition, respond() now throws a RespondError when the response_url request returns a non-2xx status (for example, an expired response_url or a rate limit). This restores the throw-on-failure behavior that axios previously provided. The RespondError (importable from @slack/bolt) carries a statusCode property with the HTTP status of the failed response. If you previously wrapped respond() in a try/catch, you will now catch a RespondError instead of an AxiosError.
If you're only calling await respond(...) without using the return value (the common case), no changes are needed.
We've upgraded the Node Slack SDK dependencies
All Node Slack SDK packages have been bumped to their next major versions.
- The
loggerpackage has been updated to v5. - The
oauthpackage has been updated to v4. - The
typespackage has been updated to v3.
Three packages have more substantial breaking changes:
- The
socket-modepackage has been updated to v3. See the guide on migrating @slack/socket-mode from v2 to v3 for handling breaking changes. - The
web-apipackage has been updated to v8. See the guide on migrating @slack/web-api from v7 to v8 for handling breaking changes.
We've improved error handling throughout
This version of Bolt leverages the new error classes from v8 of the @slack/web-api Node Slack SDK package. Errors thrown by the internal WebClient class are now proper Error subclasses.
Before (v4):
import { App } from '@slack/bolt';
const app = new App({ /* ... */ });
app.error(async ({ error }) => {
if ('code' in error && error.code === 'slack_webapi_platform_error') {
console.log((error as any).data?.error);
}
});
After (v5):
import { App } from '@slack/bolt';
import { WebAPIPlatformError, WebAPIRequestError } from '@slack/web-api';
const app = new App({ /* ... */ });
app.error(async ({ error }) => {
if (error instanceof WebAPIPlatformError) {
console.log(error.data.error); // e.g. 'channel_not_found'
} else if (error instanceof WebAPIRequestError) {
console.log(error.cause); // the underlying fetch/network error
}
});
New Features
We've added a dispatcher option to the SocketModeReceiver class
Unified proxy and TLS configuration for both WebSocket connections and HTTP API calls. Pass any undici-compatible Dispatcher instance. See the section above on the updated SocketModeReceiver class.
We've added instanceof operator support to error classes
Both Bolt's own error classes (e.g., AppInitializationError, AuthorizationError) and the underlying @slack/web-api Node Slack SDK package error classes (e.g., WebAPIPlatformError, WebAPIRequestError) now properly extend the Error class. Use the instanceof operator for type-safe error handling instead of string comparisons on the error.code property.
We've made the app.function() method the sole custom step mechanism
While the app.function() method already existed in Bolt v4, it is now the only way to handle custom function executions (replacing the removed app.step() method and WorkflowStep class). It provides complete() and fail() callback functions for signaling outcomes, and an inputs property for accessing function parameters.