Error Handling Guide
Patterns for handling errors in Teleton plugins, including the PluginSDKError class and debugging techniques.
PluginSDKError Class
All SDK methods throw PluginSDKError for known failure conditions. This class extends Error and adds a code property for programmatic handling:
class PluginSDKError extends Error {
public readonly name = "PluginSDKError" as const;
constructor(
message: string,
public readonly code: SDKErrorCode
) {
super(message);
}
}
type SDKErrorCode =
| "BRIDGE_NOT_CONNECTED"
| "WALLET_NOT_INITIALIZED"
| "INVALID_ADDRESS"
| "OPERATION_FAILED"
| "SECRET_NOT_FOUND";Import the error class and type from the SDK package:
import { PluginSDKError } from "@teleton-agent/sdk";
import type { SDKErrorCode } from "@teleton-agent/sdk";Error Codes
| Code | Thrown By | Description |
|---|---|---|
BRIDGE_NOT_CONNECTED |
sdk.telegram.* |
The Telegram client is not connected. This typically occurs during plugin startup before the GramJS bridge is ready, or if the agent loses its Telegram connection. |
WALLET_NOT_INITIALIZED |
sdk.ton.sendTON(), sdk.ton.sendJetton(), sdk.ton.verifyPayment(), sdk.ton.dex.swap*() |
The TON wallet is not configured on the agent. The admin must set up a wallet before blockchain write operations can be performed. |
INVALID_ADDRESS |
sdk.ton.sendTON(), sdk.ton.sendJetton() |
A malformed TON address was passed to a method. Valid addresses start with EQ or UQ followed by a base64-encoded string. |
OPERATION_FAILED |
Any async SDK method | A generic failure code for network errors, API timeouts, blockchain rejections, and other runtime failures. Check the message property for details. |
SECRET_NOT_FOUND |
sdk.secrets.require() |
The requested secret is not configured. None of the resolution sources (env var, secrets store, pluginConfig) returned a value. |
Error Handling Patterns
Try/Catch with instanceof
The most reliable way to catch SDK errors is using instanceof:
import { PluginSDKError } from "@teleton-agent/sdk";
async execute(params, context) {
try {
const result = await sdk.ton.sendTON(
params.address as string,
params.amount as number,
"Payment from plugin"
);
return { success: true, data: result };
} catch (err) {
if (err instanceof PluginSDKError) {
sdk.log.warn(`SDK error: [${err.code}] ${err.message}`);
return { success: false, error: err.message };
}
// Unexpected error (not from SDK)
sdk.log.error("Unexpected error during sendTON", err);
return { success: false, error: "An unexpected error occurred" };
}
}Checking Error Codes
Switch on the code property to handle specific error conditions differently:
import { PluginSDKError } from "@teleton-agent/sdk";
async execute(params, context) {
try {
await sdk.ton.sendTON(params.to as string, params.amount as number);
return { success: true, data: { sent: true } };
} catch (err) {
if (!(err instanceof PluginSDKError)) {
throw err; // Re-throw non-SDK errors
}
switch (err.code) {
case "WALLET_NOT_INITIALIZED":
await sdk.telegram.sendMessage(
context.chatId,
"The bot wallet is not configured. Ask the admin to set it up."
);
return { success: false, error: "Wallet not configured" };
case "INVALID_ADDRESS":
return {
success: false,
error: `Invalid address: ${params.to}. Use EQ... or UQ... format.`,
};
case "OPERATION_FAILED":
sdk.log.error("Transfer failed", { to: params.to, error: err.message });
return { success: false, error: `Transfer failed: ${err.message}` };
default:
return { success: false, error: err.message };
}
}
}Graceful Degradation
Check availability before calling methods that may not be ready:
async execute(params, context) {
// Check Telegram bridge before sending messages
if (!sdk.telegram.isAvailable()) {
sdk.log.warn("Telegram bridge not connected, skipping notification");
return { success: true, data: { notified: false } };
}
// Check wallet before TON operations
const address = sdk.ton.getAddress();
if (!address) {
return {
success: false,
error: "TON wallet not configured on this agent",
};
}
// Check storage before persistence
if (!sdk.storage) {
sdk.log.warn("Storage unavailable, results will not be cached");
}
// Check bot before inline operations
if (!sdk.bot) {
return {
success: false,
error: "Bot SDK not available. Add bot: { inline: true } to your manifest.",
};
}
// Check database before SQL queries
if (!sdk.db) {
return {
success: false,
error: "Database not available. Export a migrate() function.",
};
}
// All checks passed, proceed with operations
const balance = await sdk.ton.getBalance(address);
await sdk.telegram.sendMessage(
context.chatId,
`Current balance: ${balance?.balance ?? "unknown"} TON`
);
return { success: true };
}Wallet Availability Check Pattern
A common pattern for plugins that interact with the TON blockchain:
function requireWallet(sdk: PluginSDK): string {
const address = sdk.ton.getAddress();
if (!address) {
throw new Error("Wallet not configured");
}
return address;
}
async execute(params, context) {
let walletAddress: string;
try {
walletAddress = requireWallet(sdk);
} catch {
return { success: false, error: "TON wallet not configured on this agent" };
}
const balance = await sdk.ton.getBalance(walletAddress);
return { success: true, data: { address: walletAddress, balance } };
}Common Pitfalls
Do Not Catch Errors Silently
Swallowing errors makes debugging extremely difficult. Always log or return error information:
// BAD: Silent catch hides the problem
async execute(params) {
try {
await sdk.ton.sendTON(params.to as string, 1.0);
return { success: true };
} catch {
return { success: false }; // What went wrong? Nobody knows.
}
}
// GOOD: Log and return the error details
async execute(params) {
try {
await sdk.ton.sendTON(params.to as string, 1.0);
return { success: true };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
sdk.log.error("sendTON failed", { to: params.to, error: message });
return { success: false, error: message };
}
}Check isAvailable() Before Telegram Methods
During plugin startup, the Telegram bridge may not be connected yet. Calling sdk.telegram.sendMessage() during start() will throw BRIDGE_NOT_CONNECTED:
// In start() hook - bridge may not be ready
export async function start(ctx: StartContext) {
// DON'T: This might throw BRIDGE_NOT_CONNECTED
// await sdk.telegram.sendMessage(adminChatId, "Plugin started!");
// DO: Check availability first
if (sdk.telegram.isAvailable()) {
await sdk.telegram.sendMessage(adminChatId, "Plugin started!");
}
}
// In tool execute() - bridge is always ready
async execute(params, context) {
// Tools are only called when the agent is running,
// so the bridge is guaranteed to be connected here.
await sdk.telegram.sendMessage(context.chatId, "Hello!");
return { success: true };
}Check getAddress() Before TON Write Operations
sdk.ton.getAddress() returns null if no wallet is configured. Calling sendTON() or sendJetton() without a wallet throws WALLET_NOT_INITIALIZED:
async execute(params, context) {
// Pre-check avoids a thrown error
if (!sdk.ton.getAddress()) {
return {
success: false,
error: "No wallet configured. Ask admin to set up a TON wallet.",
};
}
// Read operations work without a wallet (use any address)
const balance = await sdk.ton.getBalance(params.address as string);
return { success: true, data: balance };
}sdk.db Can Be null
If your plugin does not export a migrate() function, sdk.db is null. Attempting to call methods on it will cause a runtime TypeError, not a PluginSDKError:
async execute(params) {
// This will throw TypeError: Cannot read properties of null
// sdk.db.prepare("SELECT * FROM scores");
// Always guard against null
if (!sdk.db) {
return { success: false, error: "Database not initialized" };
}
const rows = sdk.db.prepare("SELECT * FROM scores LIMIT 10").all();
return { success: true, data: rows };
}sdk.bot Can Be null
The BotSDK is only available if your manifest declares bot: { inline: true } or bot: { callbacks: true }:
// manifest.ts
export const manifest: PluginManifest = {
name: "my-plugin",
version: "1.0.0",
// Without this, sdk.bot is null:
bot: { inline: true, callbacks: true },
};
// In your plugin code
if (sdk.bot) {
sdk.bot.onInlineQuery(async (ctx) => {
return [{ id: "1", type: "article", title: "Result", content: { text: "Hello" } }];
});
sdk.bot.onCallback("action:confirm", async (ctx) => {
await ctx.answer("Confirmed!");
await ctx.editMessage("Action confirmed.");
});
} else {
sdk.log.warn("Bot SDK not available - add bot manifest to enable");
}Debugging Tips
Use sdk.log.debug() for Development
Debug messages are hidden by default. Enable them by setting the DEBUG or VERBOSE environment variable:
# Enable debug output
DEBUG=1 node bin/teleton.js start
# Or use VERBOSE for even more detail
VERBOSE=1 node bin/teleton.js startasync execute(params, context) {
sdk.log.debug("Tool invoked", {
params,
chatId: context.chatId,
senderId: context.senderId,
isGroup: context.isGroup,
});
const address = params.address as string;
sdk.log.debug("Validating address", { address });
if (!sdk.ton.validateAddress(address)) {
sdk.log.debug("Address validation failed", { address });
return { success: false, error: "Invalid address" };
}
const balance = await sdk.ton.getBalance(address);
sdk.log.debug("Balance fetched", { address, balance });
return { success: true, data: balance };
}Check Agent Logs
The Teleton agent uses Pino structured logging. Plugin log messages appear with the plugin prefix and can be filtered:
# View all logs with pino-pretty
node bin/teleton.js start 2>&1 | npx pino-pretty
# Filter for a specific plugin
node bin/teleton.js start 2>&1 | npx pino-pretty | grep "plugin:weather"
# View only errors
node bin/teleton.js start 2>&1 | npx pino-pretty --minimumLevel errorCommon Error Scenarios
| Symptom | Likely Cause | Solution |
|---|---|---|
BRIDGE_NOT_CONNECTED in start() |
Calling Telegram methods before bridge is ready | Use sdk.telegram.isAvailable() check, or move logic to tool execute() |
WALLET_NOT_INITIALIZED |
No TON wallet configured on the agent | Admin must run the setup wizard or set wallet config in config.yaml |
INVALID_ADDRESS |
User provided a wrong-format address | Validate with sdk.ton.validateAddress() before sending |
SECRET_NOT_FOUND |
Missing environment variable or secrets store entry | Set via /plugin set <name> <key> <value> or export the env var |
TypeError: Cannot read properties of null |
Accessing sdk.db, sdk.storage, or sdk.bot when they are null |
Add null checks; export migrate() for db; add bot manifest for bot SDK |
OPERATION_FAILED on swap |
Insufficient balance, no liquidity, or slippage exceeded | Check balance first, increase slippage tolerance, or try the other DEX |
| Tool not appearing in agent | Wrong scope for the chat context | Verify the tool's scope matches where you are testing (DM vs group) |