Error Handling
The SDK surfaces three distinct error categories. Wire all three callbacks to get complete coverage.
Error callbacks
Configure handlers in the AccruPay constructor:
import AccruPay from '@accrupay/node';
import type { GraphQLFormattedError } from 'graphql';
const accrupay = new AccruPay({
apiSecret: process.env.ACCRUPAY_API_SECRET!,
onAuthError: () => {
// Fired on an authentication failure (UNAUTHORIZED). Receives no argument.
// Every subsequent call will also fail — re-initialize the client.
console.error('Auth error — rotate API key');
alertOncall('accrupay_auth_failure');
},
onGraphQLError: (errors: ReadonlyArray<GraphQLFormattedError>) => {
// Fired when response.errors[] is non-empty.
for (const err of errors) {
const code = err.extensions?.code as string | undefined;
console.error(`[${code ?? 'UNKNOWN'}] ${err.message}`);
}
},
onNetworkError: (error: Error) => {
// Fired on transport-level failure (DNS, timeout, connection refused).
// No GraphQL response was received.
metrics.increment('accrupay.network_error');
},
});
GraphQL error shape
When onGraphQLError fires, each error is a GraphQLFormattedError (GraphQL spec):
interface GraphQLFormattedError {
message: string;
extensions?: {
code: string; // AccruPay error code (see below)
[key: string]: unknown;
};
locations?: { line: number; column: number }[];
path?: (string | number)[];
}
Error codes
extensions.code is one of the fixed framework codes below, or a namespaced AppError key @domain/REASON for business-rule failures.
extensions.code | Cause | Recovery |
|---|---|---|
UNAUTHORIZED | Invalid or missing API key. | Rotate the API key and re-initialize the client. |
FORBIDDEN | Valid key but insufficient permissions for the operation. | Check account permissions or contact support. |
GRAPHQL_VALIDATION_FAILED | Request arguments/input failed type or format validation. | Inspect error.message for the failing field and fix the input. |
GRAPHQL_PARSE_FAILED | The GraphQL document is malformed. | Fix the query syntax. |
@domain/REASON (AppError key) | A business rule was violated (e.g. resource not found, duplicate code). | Read message; correct the request. Do not blindly retry. |
INTERNAL_SERVER_ERROR | Unhandled server-side error. | Retry with exponential back-off; alert if it persists. |
Handling by code
onGraphQLError: (errors) => {
for (const err of errors) {
const code = err.extensions?.code as string | undefined;
switch (code) {
case 'UNAUTHORIZED':
// Re-initialize accrupay with a fresh apiSecret
break;
case 'FORBIDDEN':
// Surface permission error to the caller
break;
case 'GRAPHQL_VALIDATION_FAILED':
case 'GRAPHQL_PARSE_FAILED':
// Log the message and surface to the developer
console.warn('Invalid request:', err.message);
break;
case 'INTERNAL_SERVER_ERROR':
console.error('AccruPay server error:', err.message);
break;
default:
// Namespaced AppError business-rule key (e.g. @domain/REASON)
console.warn(`Business error [${code}]:`, err.message);
break;
}
}
},
Transaction declines are not errors
A declined or failed transaction does not throw an exception or fire onGraphQLError. The operation resolves normally — the decline is reflected in the transaction status.
Check transaction.status after every payment:
const transaction = await accrupay.transactions.clientSessions.payments.verify({
id: session.id,
});
if (['DECLINED', 'FAILED', 'ERROR'].includes(transaction.status)) {
// Normal business outcome — not an exception
const reason = transaction.providerError ?? 'No reason provided';
console.warn('Payment not successful:', reason);
// Prompt the customer to use a different payment method
}
providerError
The providerError field on a transaction contains the raw decline reason returned by the payment processor. It is useful for debugging and logging but should not be shown verbatim to end users.
// Safe for logs
console.log('Provider decline reason:', transaction.providerError);
// Map to a user-facing message instead
const userMessage = transaction.status === 'DECLINED'
? 'Your card was declined. Please try a different payment method.'
: 'Payment could not be completed. Please try again.';
BigInt serialization
Amount fields (amount, initialAmount) are bigint. JSON.stringify throws on bigint values by default.
// This throws: TypeError: Do not know how to serialize a BigInt
JSON.stringify({ amount: transaction.amount });
// Convert first
JSON.stringify({ amount: String(transaction.amount) });
// Or use a replacer
JSON.stringify(transaction, (_, v) =>
typeof v === 'bigint' ? v.toString() : v
);
Keep amounts as bigint throughout your internal logic. Only convert to string or number at serialization boundaries (API responses, logs, database writes).