ESC
Start typing to search...

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:

PluginSDKError Definition
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:

Importing PluginSDKError
import { PluginSDKError } from "@teleton-agent/sdk";
import type { SDKErrorCode } from "@teleton-agent/sdk";

Error Codes

CodeThrown ByDescription
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:

instanceof Check
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:

Error Code Branching
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:

Availability Checks
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:

Wallet Guard Pattern
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 vs Good Error Handling
// 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:

Bridge Readiness
// 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:

Wallet Pre-check
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:

Null DB Guard
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 }:

Null Bot Guard
// 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 Logging
# Enable debug output
DEBUG=1 node bin/teleton.js start

# Or use VERBOSE for even more detail
VERBOSE=1 node bin/teleton.js start
Strategic Debug Logging
async 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:

Filtering Plugin Logs
# 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 error

Common Error Scenarios

SymptomLikely CauseSolution
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)