Skip to main content

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.codeCauseRecovery
UNAUTHORIZEDInvalid or missing API key.Rotate the API key and re-initialize the client.
FORBIDDENValid key but insufficient permissions for the operation.Check account permissions or contact support.
GRAPHQL_VALIDATION_FAILEDRequest arguments/input failed type or format validation.Inspect error.message for the failing field and fix the input.
GRAPHQL_PARSE_FAILEDThe 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_ERRORUnhandled 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

warning

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
);
tip

Keep amounts as bigint throughout your internal logic. Only convert to string or number at serialization boundaries (API responses, logs, database writes).