Errors & FloodWait
Telegram's MTProto returns rich error objects with attribute-style metadata — a wait length, a target DC, a frozen account flag. Knowing the class hierarchy lets you retry the right things and surface the rest.
Hierarchy#
Everything teleproto throws from a TL request inherits from RPCError. The four branches are the four kinds of problems Telegram tells you about.
RPCError
├── InvalidDCError
├── FloodError
│ ├── FloodWaitError
│ ├── SlowModeWaitError
│ └── FloodTestPhoneWaitError
├── FrozenError
│ ├── FrozenMethodError
│ └── FrozenParticipantError
└── BadRequestError
├── UnauthorizedError
├── ForbiddenError
├── NotFoundError
├── ServerError
└── TimedOutErrorEach class carries useful attributes — .seconds on the flood waits, .newDc on the migration errors, the raw .message inherited from Error on every one of them. instanceof checks against the concrete classes are how you branch.
FloodWait#
The error you will hit most. Telegram's anti-spam decides you're calling something too often and tells you to wait .seconds before trying again. teleproto offers two ways to deal with it.
Option 1 — let the client handle it. Set floodSleepThreshold and teleproto auto-sleeps any FloodWait whose duration is under that threshold. Anything longer still throws, on the assumption that a multi-minute wait deserves your attention.
const client = new TelegramClient(session, apiId, apiHash, {
connectionRetries: 5,
floodSleepThreshold: 60, // auto-sleep on FloodWaits under 60s
});
// Short waits resolve transparently — you never see the error.
await client.sendMessage("me", { message: "hi" });Option 2 — catch and retry yourself. More control, more code. Useful when you want to record metrics, back off across multiple workers, or short-circuit after some cap.
import { FloodWaitError } from "teleproto/errors";
async function sendWithBackoff(client, peer, text) {
while (true) {
try {
return await client.sendMessage(peer, { message: text });
} catch (err) {
if (err instanceof FloodWaitError) {
console.warn(`FloodWait: sleeping ${err.seconds}s`);
await new Promise((r) => setTimeout(r, err.seconds * 1000));
continue;
}
throw err;
}
}
}The two compose: set a threshold for the small stuff, catch the long waits explicitly.
DC migration#
Telegram's users, phones, networks, and files are sharded across data centers. UserMigrateError, PhoneMigrateError, NetworkMigrateError, and FileMigrateError all carry .newDc and tell the client to reissue the request against a different DC.
teleproto handles all of these transparently. You only see one if you're invoking very low-level methods that bypass the retry loop — in which case, switch DCs with client.invoke(request, err.newDc) and resend.
Frozen accounts#
A newer Telegram concept: an account that has been restricted — read-only, no posting, no joining. teleproto exposes this as FrozenError and its two subclasses, FrozenMethodError (the call itself is blocked) and FrozenParticipantError (you can't act on that participant).
There is no retry. Surface it to the human running the automation — they need to address the freeze on their account before anything else will work.
Catching specific errors#
All errors live in teleproto/errors. Import the ones you care about and branch on instanceof — the hierarchy makes "catch all flood-likes" or "catch all permission-likes" one check apiece.
import {
FloodWaitError,
ForbiddenError,
BadRequestError,
} from "teleproto/errors";
try {
await client.sendMessage("@some-private-channel", { message: "hi" });
} catch (err) {
if (err instanceof FloodWaitError) {
await new Promise((r) => setTimeout(r, err.seconds * 1000));
} else if (err instanceof ForbiddenError) {
// not a member, banned, or channel is private — surface to the user
console.error("can't post:", err.message);
} else if (err instanceof BadRequestError) {
// your call was wrong — log and move on, don't retry
console.error("bad request:", err.message);
} else {
throw err;
}
}For advanced cases — building your own dispatcher on top of raw MTProto, or surfacing a bare Api.RpcError — there's also RPCMessageToError(rpcError, request) which turns a raw error message into the right typed class. Most code never touches it.
Logging#
teleproto's logger defaults are reasonable but loud once you're in production. Knock it down to warn and you'll hear about FloodWaits, reconnects, and unhandled updates without the per-request chatter.
client.setLogLevel("warn"); // production default
// client.setLogLevel("debug"); // chasing a specific bugLevels: none, error, warn, info, debug. Reach for debug only when you're chasing a specific bug — it dumps every TL object on the wire and the noise drowns out anything useful in steady state.