Skip to content
twitch.apiv2.0

Basics

Errors

The error classes the library throws, how to tell them apart, what a failed request carries, and what happens when Twitch answers 401.

A call that fails throws. The library turns every failure it knows about into one of a few error classes, all of which extend TwitchError, so you can decide what to do from the class and from a handful of typed fields.

import { Twitch, TwitchApiError, TwitchScopeError } from "@ix-xs/twitch.api";
const twitch = new Twitch({ clientId, clientSecret });
try {
await twitch.moderation.ban({ broadcaster_id: channelId, user_id: "9876", duration: 600 });
} catch (error) {
if (error instanceof TwitchScopeError) {
console.log("Ask the user for", error.required);
} else if (error instanceof TwitchApiError && error.status === 400) {
console.log("Twitch refused:", error.message);
} else {
throw error;
}
}
ClassExtendsThrown when
TwitchErrorErrorBase class. Also thrown as is in a few cases: no clientId, no fetch in the runtime, a response that is not JSON.
TwitchApiErrorTwitchErrorTwitch answered with an error status, and retrying was not possible or did not help.
TwitchRateLimitErrorTwitchApiErrorA 429 that was not retried, or the rate limit was reached with rateLimit: "throw".
TwitchAuthErrorTwitchErrorNo usable token: no user registered, several users and none chosen, an acting user that cannot be guessed, no client secret, a token that died and cannot be renewed.
TwitchScopeErrorTwitchAuthErrorThe chosen user token lacks the scope the endpoint needs. Thrown before the request is sent.
TwitchTimeoutErrorTwitchErrorAn attempt took longer than timeout, and could not be retried or was the last retry.
TwitchNetworkErrorTwitchErrorThe request never reached Twitch, or the connection broke before the whole answer arrived, with the same condition on retries.
TwitchEventSubErrorTwitchErrorAn EventSub listener failed: see Events overview.

Requests to the OAuth server (getting the app token, refreshing, validating or revoking a token, the sign-in flows) throw the same classes: TwitchApiError for an error status, TwitchTimeoutError and TwitchNetworkError when the server cannot be reached.

The error reference lists the fields of each class.

Use instanceof. Two classes have a subclass, so test the subclass first when you handle both: TwitchRateLimitError before TwitchApiError, TwitchScopeError before TwitchAuthError.

import {
TwitchApiError,
TwitchAuthError,
TwitchError,
TwitchNetworkError,
TwitchRateLimitError,
TwitchScopeError,
TwitchTimeoutError,
} from "@ix-xs/twitch.api";
function describe(error: unknown): string {
if (error instanceof TwitchScopeError) return `missing scope: ${error.required.join(" or ")}`;
if (error instanceof TwitchAuthError) return `no usable token: ${error.message}`;
if (error instanceof TwitchRateLimitError) return `rate limited for ${error.retryAfter} ms`;
if (error instanceof TwitchApiError) return `Twitch answered ${error.status}: ${error.message}`;
if (error instanceof TwitchTimeoutError || error instanceof TwitchNetworkError) return "Twitch is unreachable";
if (error instanceof TwitchError) return error.message;
throw error; // not from the library
}

Each class also sets error.name to its own name ("TwitchApiError", "TwitchScopeError"), which is handy in logs. TwitchNetworkError and TwitchTimeoutError keep the original failure in error.cause.

A TwitchApiError carries the whole answer.

FieldTypeContent
statusnumberHTTP status code.
errorstring | undefinedShort label sent by Twitch, such as Bad Request or Unauthorized.
messagestringThe message sent by Twitch, followed by the status, the method and the path: Missing required parameter (400 on GET /helix/users).
methodstringHTTP method of the request.
urlstringFull URL of the request, with its query string.
bodyunknownThe parsed response body. The raw text when it is not JSON, undefined when there is none.
headersHeaders | undefinedResponse headers.
import { TwitchApiError } from "@ix-xs/twitch.api";
try {
await twitch.polls.create({ title: "Next game?", choices: ["Hades", "Celeste"], duration: 120 });
} catch (error) {
if (!(error instanceof TwitchApiError)) throw error;
console.error(error.status, error.error, error.message);
console.error(error.method, error.url, error.body);
}

The statuses you meet most often:

StatusUsual meaning
400A parameter is missing or invalid, or the action is not possible right now (a poll is already running, the channel is offline).
401The token is not allowed to make this call, for example a broadcaster_id that is not the owner of the token.
403The user lacks a permission on Twitch’s side, for example they are not a moderator of the channel.
404The thing you named does not exist.
409A conflict, such as an EventSub subscription that already exists.
429Rate limit, as a TwitchRateLimitError. See Rate limits and retries.
5xxA failure on Twitch’s side, raised after the retries are used up.

Finding nothing is not an error. Lookups that can come back empty resolve with null, so the type forces you to handle it:

const user = await twitch.users.getByLogin("some_login");
if (!user) console.log("No such user");
const sub = await twitch.subscriptions.check({ broadcaster_id: channelId });
console.log(sub ? `Subscribed at tier ${sub.tier}` : "Not subscribed");

The methods that do this are users.getById, users.getByLogin, teams.get, subscriptions.check, charity.campaign, chat.sharedSession, chat.pinnedMessage, guestStar.session and extensions.get. For teams.get and subscriptions.check, Twitch answers 404, and the library turns that into null.

Lists behave the same way: users.get with IDs that do not exist resolves with a shorter or empty array, and paginator.first() resolves with undefined on an empty list.

One call reports a refusal in its result instead of throwing: chat.send resolves with is_sent: false and a drop_reason when Twitch does not deliver the message.

Twitch answers 401 in two different situations: the token is dead (expired or revoked), or the token is fine but not allowed to do this. The client tells them apart by asking Twitch to validate the token, then acts on the result:

The token isWhat the client does
ValidThrows the TwitchApiError as Twitch sent it. Nothing is refreshed.
A dead app tokenRequests a new app token and sends the call again.
A dead user token with a refresh tokenRefreshes it, saves it, emits tokenRefresh and sends the call again.
A dead user token without a refresh tokenForgets the user, emits tokenInvalid and throws a TwitchAuthError.
A dead user token whose refresh token Twitch rejectsForgets the user, emits tokenInvalid and throws a TwitchAuthError saying the user has to authorize the application again.
Passed with { token }Throws the TwitchApiError. The library does not manage that token.

The call is sent again once. A second 401 is thrown as a TwitchApiError. This second attempt does not count against retries, and it applies to every HTTP method, since Twitch did nothing with the request it rejected.

A valid token that gets a 401 usually means the wrong user is acting. The message says so:

TwitchApiError: The ID in broadcaster_id must match the user ID in the token (401 on GET /helix/users/blocks)

Acting as a user explains how the token is chosen, and Managing users what to do when tokenInvalid fires.

This wrapper covers what a bot command usually needs: tell the user when the action is refused, stay quiet and retry later on a temporary failure, and let everything else surface.

import {
TwitchApiError,
TwitchAuthError,
TwitchNetworkError,
TwitchRateLimitError,
TwitchTimeoutError,
} from "@ix-xs/twitch.api";
type Outcome = { ok: true } | { ok: false; reason: string; retryInMs?: number };
async function timeoutUser(target: string, seconds: number): Promise<Outcome> {
try {
await twitch.moderation.ban({ broadcaster_id: channelId, user_id: target, duration: seconds });
return { ok: true };
} catch (error) {
if (error instanceof TwitchRateLimitError) {
return { ok: false, reason: "rate limited", retryInMs: error.retryAfter };
}
if (error instanceof TwitchApiError && error.status < 500) {
return { ok: false, reason: error.message }; // refused: bad target, not a moderator...
}
if (error instanceof TwitchAuthError) {
return { ok: false, reason: "the bot has to sign in again or lacks a scope" };
}
if (error instanceof TwitchApiError || error instanceof TwitchTimeoutError || error instanceof TwitchNetworkError) {
return { ok: false, reason: "Twitch is having trouble", retryInMs: 5000 };
}
throw error;
}
}

Two kinds of failure pass through unchanged:

  • When you cancel a call with an AbortSignal, the call rejects with the reason of the signal, which is an AbortError unless you gave another one.
  • An error thrown by your own token store reaches the call that triggered it.