LangChain Tutorial (TypeScript): implementing retry logic for advanced developers
By Cyprian AaronsUpdated 2026-04-21
langchainimplementing-retry-logic-for-advanced-developerstypescript
This tutorial shows how to add retry logic around LangChain calls in TypeScript without turning your agent into a noisy loop machine. You’ll build a production-friendly wrapper that retries transient failures, respects rate limits, and keeps failed attempts visible enough for debugging.
What You'll Need
- •Node.js 18+
- •TypeScript 5+
- •
langchain - •
@langchain/openai - •An OpenAI API key in
OPENAI_API_KEY - •A project configured with ESM or
ts-node/tsx - •Basic familiarity with LangChain chat models and async/await
Install the packages:
npm install langchain @langchain/openai
npm install -D typescript tsx @types/node
Step-by-Step
- •Start with a plain LangChain model call so you have a baseline to wrap.
The retry logic should sit outside the model call, not inside your prompt construction.
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage } from "@langchain/core/messages";
const model = new ChatOpenAI({
model: "gpt-4o-mini",
temperature: 0,
});
async function main() {
const response = await model.invoke([
new HumanMessage("Write one sentence about retries in distributed systems."),
]);
console.log(response.content);
}
main().catch(console.error);
- •Add a reusable retry helper with exponential backoff and jitter.
This version retries only on transient errors like rate limits, timeouts, and 5xx-style failures.
type RetryOptions = {
maxAttempts: number;
baseDelayMs: number;
maxDelayMs: number;
};
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isRetryableError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
return /rate limit|timeout|timed out|429|500|502|503|504/i.test(message);
}
async function withRetry<T>(
fn: () => Promise<T>,
options: RetryOptions
): Promise<T> {
let lastError: unknown;
for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (!isRetryableError(error) || attempt === options.maxAttempts) {
throw error;
}
const backoff = Math.min(
options.maxDelayMs,
options.baseDelayMs * Math.pow(2, attempt - 1)
);
const jitter = Math.floor(Math.random() * backoff * 0.2);
const delay = backoff + jitter;
console.warn(`Attempt ${attempt} failed. Retrying in ${delay}ms...`);
await sleep(delay);
}
}
throw lastError;
}
- •Wrap the LangChain invocation with the retry helper.
Keep the wrapper narrow so you can reuse it for chat models, tool calls, or chain execution later.
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage } from "@langchain/core/messages";
const model = new ChatOpenAI({
model: "gpt-4o-mini",
temperature: 0,
});
async function main() {
const result = await withRetry(
() =>
model.invoke([
new HumanMessage("Explain why retry logic needs jitter in one sentence."),
]),
{
maxAttempts: 4,
baseDelayMs: 500,
maxDelayMs: 4000,
}
);
console.log(result.content);
}
main().catch((error) => {
console.error("Final failure:", error);
});
- •Use the same pattern around chains, not just raw model calls.
In LangChain, any async runnable can be wrapped this way as long as the failure is transient and safe to repeat.
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0 });
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a concise technical writer."],
["human", "{topic}"],
]);
const chain = prompt.pipe(model).pipe(new StringOutputParser());
async function main() {
const answer = await withRetry(
() => chain.invoke({ topic: "Describe idempotency in payment APIs." }),
{ maxAttempts: 3, baseDelayMs: 300, maxDelayMs: 3000 }
);
console.log(answer);
}
main().catch(console.error);
- •Make retries safe by classifying operations before you wrap them.
Retrying a read-only generation is fine; retrying a payment submission or ticket creation without idempotency keys is how you create duplicates.
type OperationKind = "read" | "write";
function runWithPolicy<T>(
kind: OperationKind,
task: () => Promise<T>
): Promise<T> {
if (kind === "write") {
return task();
// For writes, use idempotency keys and explicit compensating actions.
// Do not blindly retry side-effecting operations.
}
return withRetry(task, {
maxAttempts: Math.min(5, process.env.NODE_ENV === "production" ? undefined : undefined) as any,
baseDelayMs: kind === "read" ? undefined : undefined,
maxDelayMs: undefined as any,
});
}
The previous snippet shows the policy boundary, but keep your actual implementation explicit rather than clever. In production code, define separate settings for reads and writes instead of trying to infer behavior from context.
A clean version looks like this:
async function runReadOnly<T>(task: () => Promise<T>) {
return withRetry(task, { maxAttempts:
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
By Cyprian Aarons, AI Consultant at Topiax.
Want the complete 8-step roadmap?
Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.
Get the Starter Kit