-
Understand the Plaid error structure. Every Plaid error has these fields:
{
"error_type": "ITEM_ERROR",
"error_code": "ITEM_LOGIN_REQUIRED",
"error_message": "the login details of this item have changed...",
"display_message": "Please update your credentials.",
"request_id": "abc123",
"causes": [],
"status": 400
}
-
Catch and classify errors in code:
import { PlaidError } from "plaid";
try {
const response = await plaidClient.transactionsSync({
access_token: accessToken,
cursor: cursor,
});
} catch (error: any) {
if (error.response?.data) {
const plaidError = error.response.data as PlaidError;
await handlePlaidError(plaidError, itemId);
} else {
console.error("Network error:", error.message);
}
}
-
Error types and recovery strategies:
| Error Type | Error Code | Cause | Recovery |
|---|
ITEM_ERROR | ITEM_LOGIN_REQUIRED | User changed password at bank | Launch Link in update mode |
ITEM_ERROR | ITEM_NOT_FOUND | Item was removed or doesn't exist | Remove from your database |
ITEM_ERROR | ACCESS_NOT_GRANTED | User didn't grant required permission | Re-link with correct products |
INVALID_REQUEST | INVALID_ACCESS_TOKEN | Token is malformed or expired | Re-link the item |
INVALID_INPUT | INVALID_CREDENTIALS | Wrong client_id or secret | Check environment variables |
RATE_LIMIT_EXCEEDED | RATE_LIMIT | Too many requests | Implement exponential backoff |
API_ERROR | INTERNAL_SERVER_ERROR | Plaid service issue | Retry with backoff |
INSTITUTION_ERROR | INSTITUTION_NOT_RESPONDING | Bank is down | Retry later, notify user |
INSTITUTION_ERROR | INSTITUTION_DOWN | Bank is offline | Retry later |
-
Implement an error handler:
async function handlePlaidError(error: PlaidError, itemId: string) {
switch (error.error_type) {
case "ITEM_ERROR":
if (error.error_code === "ITEM_LOGIN_REQUIRED") {
await db.items.update({
where: { item_id: itemId },
data: { status: "LOGIN_REQUIRED" },
});
await notifyUser(itemId, "Your bank connection needs to be updated.");
}
break;
case "RATE_LIMIT_EXCEEDED":
await sleep(Math.pow(2, retryCount) * 1000);
break;
case "INSTITUTION_ERROR":
await db.items.update({
where: { item_id: itemId },
data: { status: "INSTITUTION_ERROR", last_error: error.error_code },
});
break;
case "API_ERROR":
console.error(`Plaid API error: ${error.error_code}`);
break;
}
}
-
Handle ITEM_LOGIN_REQUIRED with update mode. This is the most common production error. When a user changes their bank password, Plaid can't access the account:
const response = await plaidClient.linkTokenCreate({
user: { client_user_id: userId },
client_name: "My App",
country_codes: [CountryCode.Us],
language: "en",
access_token: brokenAccessToken,
});
-
Implement retry with backoff:
async function callWithRetry<T>(
fn: () => Promise<T>,
maxRetries = 3
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
const plaidError = error.response?.data;
const retryable =
plaidError?.error_type === "RATE_LIMIT_EXCEEDED" ||
plaidError?.error_type === "API_ERROR" ||
plaidError?.error_type === "INSTITUTION_ERROR";
if (!retryable || attempt === maxRetries) throw error;
const delay = Math.pow(2, attempt) * 1000 + Math.random() * 500;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error("Unreachable");
}
-
Monitor errors via webhooks. Plaid sends ITEM/ERROR webhooks when items break:
if (webhook_type === "ITEM" && webhook_code === "ERROR") {
const { error } = req.body;
await handlePlaidError(error, item_id);
}