Repository navigation
Slack Errors
Slack throws SlackError for invalid configuration, local request
validation, and vendor responses.
import { SlackError, SlackErrorCodes } from '@tundraconnect/slack/errors';
const error = new SlackError('NOT_FOUND', { vendorError: 'channel_not_found' });
console.log(error.message);
console.log(SlackErrorCodes.NOT_FOUND);Unlike almost every other connect in this repository, Slack signals most
documented failures with HTTP 200 OK and
{ ok: false, error: '<short_error_code>' } in the response body — not a
4xx/5xx status. Slack's internal response handler therefore reads the
body on every call (success and failure alike), maps the error string
to a stable connect-specific code (see the table below), and only falls
back to a genuine HTTP status for real transport-level failures:
- A real HTTP
429(with Slack's documentedRetry-Afterheader) always throwsRATE_LIMITED, regardless of the body. - A real HTTP
5xxalways throwsSERVICE_UNAVAILABLE. - Otherwise,
{ ok: false, error }in the body is mapped via the table below. - A non-2xx status Slack didn't explain via its own envelope (e.g. an
intermediating proxy's error page) falls back to
UNKNOWN_ERROR.
The raw vendor error string always survives on the thrown error's context
as vendorError, even when it was mapped to a specific code — see
Reading diagnostic metadata below.
| Code | Meaning |
|---|---|
CONFIG_INVALID_TOKEN |
auth is missing, isn't type: 'BEARER', or its token is blank or not a string. |
INVALID_REQUEST |
A request failed local schema validation before being sent, or Slack rejected its shape/content (e.g. invalid_blocks, msg_too_long, invalid_cursor, invalid_limit, is_archived). |
RESPONSE_ERROR |
A response whose ok wasn't false still failed schema validation. |
AUTH_FAILED |
Slack reported invalid_auth, not_authed, account_inactive, token_revoked, or token_expired. |
FORBIDDEN |
Slack reported missing_scope, no_permission, access_denied, restricted_action, cant_update_message, cant_delete_message, user_not_visible, or ekm_access_denied. |
NOT_FOUND |
Slack reported channel_not_found, user_not_found, or message_not_found. |
NOT_IN_CHANNEL |
Slack reported not_in_channel — the bot isn't a member of the target conversation. |
RATE_LIMITED |
A real HTTP 429, ok: false with error: 'ratelimited'/'rate_limited', or RESTler's exhausted maxRetryWait retry. Every path carries retryAfterSeconds (number or absent) and retryAfter (the same number, or 'a few' without a Retry-After hint). |
SERVICE_UNAVAILABLE |
A real HTTP 5xx, or ok: false with error: 'internal_error'/'service_unavailable'/'fatal_error'. |
TIMEOUT |
Slack did not answer within the client timeout (timeoutSeconds in context). |
NETWORK_ERROR |
fetch failed before any response (DNS, TLS, connection reset); the transport error is the cause. |
UNKNOWN_ERROR |
An undocumented error string, or a non-2xx status Slack didn't explain via its own envelope. |
The resolved code is also available as a public, readonly error.code
property — branch on failure mode without matching against .message:
if (error instanceof SlackError && error.code === 'NOT_IN_CHANNEL') {
// invite the bot to the channel, then retry
}Use getContextValue() to read diagnostic metadata:
try {
await client.postMessage({ channel: 'C123ABC456', text: 'hi' });
} catch (error) {
if (error instanceof SlackError) {
console.log(error.getContextValue('vendorError')); // e.g. 'channel_not_found'
console.log(error.getContextValue('status')); // HTTP status, when relevant
console.log(error.getContextValue('retryAfter')); // RATE_LIMITED only
}
}TIMEOUT, NETWORK_ERROR, SERVICE_UNAVAILABLE and RATE_LIMITED mean
"no answer yet": retrying later can help. Every other code is a definite
refusal or a misconfiguration that retrying will not fix. Branch on the
readonly err.transient, which is true for exactly these four, rather
than listing codes yourself. The set is also exported as
SLACK_TRANSIENT_CODES from @tundraconnect/slack/errors.
import { SlackError } from '@tundraconnect/slack/errors';
declare function callTheClient(): Promise<unknown>;
try {
await callTheClient();
} catch (err) {
if (err instanceof SlackError && err.transient) {
// queue it and try again later
}
throw err;
}| Context | Present on | Meaning |
|---|---|---|
timeoutSeconds |
TIMEOUT |
The deadline that was missed, in seconds. |
A rate-limited request throws RATE_LIMITED. Two context values tell you what to
do next:
| Context | Meaning |
|---|---|
retryAfterSeconds |
Seconds the vendor asked you to wait, parsed by RESTler from Retry-After (delta seconds or an HTTP-date), X-RateLimit-Reset-After, RateLimit-Reset, or an epoch-seconds X-RateLimit-Reset. undefined when the response carried none — never a guess. |
retried |
Set only when maxRetryWait is configured: true if RESTler already waited once and was throttled again, false if it did not wait (the hint exceeded the cap, or there was no hint to wait on). |
Nothing is retried by default. Opt in with two client options (seconds,
each 0–120):
-
maxRetryWait— when a 429 carries a hint no longer than this, RESTler waits that long and retries once; otherwise it throwsRATE_LIMITEDimmediately.0disables the retry. -
defaultRetryWait— the wait used when a 429 carries no hint. Without it a hintless 429 is never retried: RESTler does not invent a delay. Only consulted whenmaxRetryWaitis set, and still capped by it.
| Code | Raised when |
|---|---|
WEBHOOK_INVALID_HEADERS |
A required signature header is missing. |
WEBHOOK_TIMESTAMP_INVALID |
Unparseable timestamp, or outside the tolerance window. |
WEBHOOK_SIGNATURE_INVALID |
Treat the request as forged. |
- Algolia
- AzureBlob
- CloudflareDNS
- CloudflareEmail
- CloudflareSaaS
- CloudflareTurnstile
- CoinGecko
- Discord
- DodoPayments
- GCS
- GoogleAnalytics
- GoogleCertificateManager
- GoogleWebRisk
- Kalshi
- ntfy
- OpenExchange
- OpenWeatherMap
- PayPal
- Polymarket
- Razorpay
- Resend
- S3
- SendGrid
- Sentry
- Slack
- Stripe
- Telegram
- Twilio
- UpstashRedis
- URLhaus