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 branches follow Telegram's HTTP-style error codes, and every concrete server error hangs off one of them as its own generated class.
RPCError base for everything the server throws
BadRequestError (400) ChannelPrivateError, MessageNotModifiedError, ...
UnauthorizedError (401) SessionRevokedError, AuthKeyUnregisteredError, ...
ForbiddenError (403) ChatWriteForbiddenError, ...
NotFoundError (404)
FloodError (420) FloodWaitError, SlowModeWaitError, ...
ServerError (500)
TimedOutError (503)
InvalidDCError UserMigrateError, PhoneMigrateError, ...Each class carries useful attributes — .seconds on the flood waits, .newDc on the migration errors, and on every one of them .errorMessage with the exact server string (FLOOD_WAIT_42, SESSION_REVOKED) if you ever need to match literally. instanceof checks against the concrete classes are how you branch.
Every RPC error is a class#
Since v1.227, every error in Telegram's official error database is generated as its own class — SessionRevokedError, ChannelPrivateError, AuthKeyUnregisteredError, UserDeactivatedError, hundreds more. No string matching, no err.message.includes(...) — just catch and instanceof.
import { errors } from "teleproto";
// or: import { SessionRevokedError } from "teleproto/errors";
try {
await client.start({ /* resolvers */ });
} catch (err) {
if (err instanceof errors.SessionRevokedError) {
// the saved session was terminated from another device — re-login
} else if (err instanceof errors.AuthKeyUnregisteredError) {
// auth key is gone server-side — wipe the session and re-login
} else if (err instanceof errors.UnauthorizedError) {
// any other 401 — including codes newer than this teleproto build
console.error("auth failed:", err.errorMessage);
} else {
throw err;
}
}Errors Telegram invents afteryour teleproto build was generated don't vanish — they fall back to the base class for their HTTP code (UnauthorizedError, FloodError, BadRequestError…), and .errorMessage still carries the exact server string. Catch the base class as your safety net, the concrete class for the cases you handle specially.
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. Frozen-account errors are mapped by their HTTP code like every other typed error — catch errors.FrozenMethodInvalidError (the call itself is blocked) or errors.FrozenParticipantMissingError (you can't act on that participant). The pre-1.227 names FrozenError / FrozenMethodError / FrozenParticipantError remain as deprecated aliases, so existing catch code keeps working.
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.
Shipping logs somewhere structured? The built-in Logger is pluggable: assign a handler and every record arrives as { level, message, error, date } instead of being printed. Error stacks route through it too — nothing bypasses your pipeline to write straight to the console.
import { Logger } from "teleproto";
// Route teleproto's internal logs into your own logging stack.
const logger = new Logger("warn");
logger.handler = ({ level, message, error, date }) => {
myPino[level]({ err: error, time: date.getTime() }, message);
};
const client = new TelegramClient(session, apiId, apiHash, {
connectionRetries: 5,
baseLogger: logger,
});