How to Handle Errors and Implement Retry Logic in LINEJS: A Complete Guide

LINEJS provides a layered error-handling strategy through the RequestClient class that automatically refreshes expired tokens, wraps API errors in InternalError objects, and supports custom exponential-backoff retry patterns via the RetryPolicy interface.

LINEJS is a TypeScript client for the official LINE API that communicates via Thrift binary requests. Understanding how to handle errors and implement retry logic in LINEJS ensures your bot remains resilient against token expirations, network glitches, and transient API failures without manual intervention.

Understanding the LINEJS Error Architecture

The library implements a four-layer error handling stack within the RequestClient class located in packages/linejs/base/request/mod.ts.

Low-level Thrift parsing occurs at lines 70-78, where binary responses convert to JavaScript objects and throw generic Error instances for malformed buffers.

Typed API error translation happens at lines 30-44, where LINE-specific structs like TalkException or SquareException transform into standardized InternalError objects that preserve the original error metadata.

Automatic token refresh triggers at lines 51-63 when the response contains MUST_REFRESH_V3_TOKEN, silently refreshing the access token and re-issuing the request once.

E2EE decryption retry operates in packages/linejs/base/e2ee/mod.ts (lines 79-100), attempting a second decryption with a fresh Decipher object if the first attempt fails.

The InternalError Class

All API-specific errors unify under the InternalError class defined in packages/linejs/base/core/utils/error.ts. This class extends the native Error object and exposes three critical properties:

export class InternalError extends Error {
  constructor(
    readonly type: string,
    override readonly message: string,
    readonly data: Record<string, LooseType> = {},
  ) {
    super(message);
    this.name = type;
    this.data = data;
  }
}
  • type stores the exception name (e.g., "TalkException", "SquareException")
  • message provides the human-readable description
  • data contains the parsed Thrift struct including error codes and retry policies

Handling Specific LINE API Errors

Use instanceof checks to distinguish API errors from network or parsing failures:

import { InternalError } from "@evex/linejs/base/core/utils/error";

try {
  const profile = await client.talk.getProfile({ userMid: "Uxxxxxxxxxxxxxxxxxxxxxx" });
  console.log("Profile:", profile);
} catch (e) {
  if (e instanceof InternalError) {
    console.error(`LINE API error [${e.type}] – code: ${e.data.code}`, e.data);
    
    // Access server-sent retry policy if available
    if (e.data.retryPolicy) {
      const policy = e.data.retryPolicy;
      // Implement backoff using policy.initialDelayInMillis, etc.
    }
  } else {
    console.error("Unexpected error:", e);
  }
}

Built-In Automatic Retry Mechanisms

Token Refresh Flow

When requestCore detects res.data.e?.code === "MUST_REFRESH_V3_TOKEN" and a stored refresh token exists, the client:

  1. Sets isRefresh to true
  2. Calls client.auth.tryRefreshToken()
  3. Re-issues the identical request with isReRequest = true
  4. Returns the second attempt to your code transparently

You never need to handle token renewal manually; the RequestClient manages this entirely within packages/linejs/base/request/mod.ts.

E2EE Decryption Retry

The end-to-end encryption module in packages/linejs/base/e2ee/mod.ts (lines 79-100) implements internal resilience. If crypto.createDecipheriv throws on the first attempt, the code creates a fresh decipher2 instance and retries. Only if the second attempt fails does the error propagate to your application.

Implementing Custom Retry Strategies

Using the RetryPolicy Interface

The LINE API can return a RetryPolicy object defining exponential backoff parameters. This interface, generated from the Thrift schema in packages/types/line_types.ts, specifies:

export interface RetryPolicy {
  initialDelayInMillis: Int64;
  maxDelayInMillis: Int64;
  multiplier: number;
  jitterRate: number;
}

Extract this policy from InternalError.data to implement server-directed backoff behavior.

Custom Exponential Backoff Implementation

Apply the policy values to create a compliant retry loop:

import type { RetryPolicy } from "@evex/linejs/types";

async function callWithPolicy<T>(fn: () => Promise<T>, policy: RetryPolicy) {
  let delay = Number(policy.initialDelayInMillis);
  const max = Number(policy.maxDelayInMillis);
  
  while (true) {
    try {
      return await fn();
    } catch (err) {
      if (delay > max) throw err;
      
      await new Promise(r => setTimeout(r, delay));
      delay = Math.min(delay * policy.multiplier, max);
      delay += delay * policy.jitterRate * Math.random();
    }
  }
}

The User-Level Retry Helper

For simple use cases, copy the retryApiCall utility from example/square/pollingSquareChatEvents.ts (lines 43-57):

async function retryApiCall<T>(fn: () => Promise<T>, retries = 4, delay = 3000): Promise<T> {
  let attempt = 0;
  while (true) {
    try {
      return await fn();
    } catch (e) {
      if (++attempt > retries) {
        throw new Error('retryApiCall: exhausted retries');
      }
      console.warn(`Retry ${attempt}/${retries} after ${delay}ms –`, e);
      await new Promise((r) => setTimeout(r, delay));
    }
  }
}

This helper catches any thrown error including InternalError, retries the async function up to retries times, and waits delay milliseconds between attempts.

Practical Implementation Patterns

Combine built-in token refresh with user-level retry for maximum resilience against both authentication and transient network failures:

import { LineClient } from "@evex/linejs";
import { retryApiCall } from "./retryHelper"; // copy from example/square/pollingSquareChatEvents.ts

const client = new LineClient({ /* auth config */ });

async function fetchSquareEvents() {
  // Token refresh happens automatically inside RequestClient
  // We add high-level retry for network glitches
  return await retryApiCall(() =>
    client.square.fetchMyEvents({ limit: 200 }),
    3,      // 3 attempts
    2000    // 2-second pause
  );
}

Summary

Frequently Asked Questions

How does LINEJS handle expired access tokens?

When the API returns the MUST_REFRESH_V3_TOKEN error code, the RequestClient class in packages/linejs/base/request/mod.ts automatically calls client.auth.tryRefreshToken() and re-issues the failed request once. This happens transparently at lines 51-63, so your code receives the successful response without handling the refresh manually.

Can I customize the retry behavior for specific API errors?

Yes. Catch InternalError instances and inspect error.data.retryPolicy to extract the server's recommended backoff parameters. Use these values—initialDelayInMillis, maxDelayInMillis, multiplier, and jitterRate—to implement custom exponential backoff logic, or adapt the retryApiCall helper from the examples to check error types before retrying.

What is the difference between InternalError and standard JavaScript errors?

InternalError extends the native Error class and adds type and data properties. While standard errors indicate programming mistakes or network failures, InternalError specifically represents LINE API business logic failures (like permission denied or rate limiting) and contains the parsed Thrift error struct for programmatic handling.

Where can I find a working example of retry logic in the LINEJS repository?

The file example/square/pollingSquareChatEvents.ts contains a production-ready retryApiCall function at lines 43-57. This utility demonstrates wrapping arbitrary API calls with a fixed-delay retry mechanism that you can extend to support exponential backoff or conditional retry based on error types.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →