Reactor Error Handling
Errors that occur when invoking a reactor will be formatted according to the standard Basis Theory Error Response. The contents of each error will vary based on the failure scenario, and there are several standard error codes that are possible that can be used to determine the cause of the error.
node-bt Error Handling
The @basis-theory/basistheory-reactor-formulas-sdk-js npm package can be referenced within your reactor code to better control the status codes and messages included on reactor errors.
Error Types
This package includes several error types that can be thrown from your code, including:
| Type | Status Code | Detail |
|---|---|---|
AuthenticationError | 401 | Authentication Failed |
AuthorizationError | 403 | Forbidden |
BadRequestError | 400 | Bad Request |
InvalidPaymentMethodError | 402 | Invalid Payment Method |
RateLimitError | 429 | Rate Limit Exceeded |
ReactorRuntimeError | 500 | Reactor Runtime Error |
ServiceUnavailableError | 503 | Service Unavailable Error |
CustomHttpResponseError | any | N/A |
If your reactor code throws any of these standard error types, with the exception of CustomHttpResponseError, they will be caught and translated into
a standard Basis Theory error response with the corresponding status code and detail specified in the table above.
If a CustomHttpResponseError is thrown from a reactor, the status code, headers, and body defined by the error will be used to construct a custom API response.
Error Constructors
Each of the error types, excluding CustomHttpResponseError, accepts a constructor argument that will be returned within the errors block.
The constructor argument for each type supports several error formats, for example:
- String
- Array
- Object
// Throwing an error constructed with a string error message:
const { ReactorRuntimeError } = require('@basis-theory/basis-theory-reactor-formulas-sdk-js');
throw new ReactorRuntimeError('My error message');
// Would result in the following error response:
{
"status": 500,
"detail": "Reactor Runtime Error",
"errors": {
"error": ["My error message"]
}
}
// Throwing an error constructed with an array:
const { ReactorRuntimeError } = require('@basis-theory/basis-theory-reactor-formulas-sdk-js');
throw new ReactorRuntimeError(["error 1", "error 2"]);
// Would result in the following error response:
{
"status": 500,
"detail": "Reactor Runtime Error",
"errors": {
"error": ["error 1", "error 2"]
}
}
// Throwing an error constructed with an object:
const { ReactorRuntimeError } = require('@basis-theory/basis-theory-reactor-formulas-sdk-js');
throw new ReactorRuntimeError({
error1: 'description 1',
error2: ['description 2', 'description 3']
});
// Would result in the following error response:
{
"status": 500,
"detail": "Reactor Runtime Error",
"errors": {
"error1": ["description 1"],
"error2": ["description 2", "description 3"]
}
}
Standard Error Responses
Standard error types (excluding CustomHttpResponseError) are caught and translated into a standard Basis Theory error response with the corresponding status code and detail.
For example, throwing an AuthenticationError:
const { AuthenticationError } = require('@basis-theory/basis-theory-reactor-formulas-sdk-js');
module.exports = async function (req) {
if (!req.args.apiKey) {
throw new AuthenticationError('API key is required');
}
// ...
};
would result in the following error response with a 401 status code:
{
"status": 401,
"detail": "Authentication Failed",
"errors": {
"error": ["API key is required"]
}
}
Custom Error Responses
To have complete control over the response body that is returned, including custom status codes, custom headers,
or to have complete control over the response body, a CustomHttpResponseError can be thrown from a reactor.
For example, throwing the following error:
const { CustomHttpResponseError } = require('@basis-theory/basis-theory-reactor-formulas-sdk-js');
throw new CustomHttpResponseError({
status: 400,
headers: {
"Custom-Response-Header-1": "custom-value-1",
"Custom-Response-Header-2": "custom-value-2"
},
body: {
"custom_property": "value",
"custom_object": {
"custom_nested_property": "value"
}
}
});
results in an API response with status code 400 having the headers:
Custom-Response-Header-1: custom-value-1
Custom-Response-Header-2: custom-value-2
and the response body:
{
"custom_property": "value",
"custom_object": {
"custom_nested_property": "value"
}
}
Uncaught Errors
Any other errors raised from reactor code will result in a ReactorRuntimeError containing the original error object
and the response will have status code 500.
node22 Error Handling
The node22 runtime gives you full control over error responses through the return value. Return an appropriate statusCode and error details in the body:
Standard Error Responses
Return a response object with statusCode and body to handle errors. The response format is exactly what you specify. There is no automatic wrapping.
For example:
module.exports = async function (event) {
const { req, logger } = event;
if (!req.card_number) {
logger.warn("Missing required field: card_number");
return {
res: {
body: { error: "card_number is required" },
statusCode: 400
}
};
}
// ...
};
This results in an error response with status code 400:
{
"error": "card_number is required"
}
Custom Error Responses
In node22, you always have full control over error responses. Simply return the desired statusCode, headers, and body in your response object. There is no wrapper or special error type needed. The response you return is sent directly to the client.
For example:
module.exports = async function (event) {
return {
res: {
statusCode: 400,
headers: {
"Custom-Response-Header-1": "custom-value-1",
"Custom-Response-Header-2": "custom-value-2"
},
body: {
myCustomError: "My custom error message"
}
}
};
};
This results in an API response with status code 400, having the specified headers and response body:
{
"myCustomError": "My custom error message"
}
Uncaught Errors
If an uncaught error is thrown from your node22 reactor code (e.g., a runtime exception that isn't caught), it will result in a 500 status code with a generic error response. To provide better error handling, wrap your code in try/catch blocks and return appropriate error responses:
module.exports = async function (event) {
const { req, logger } = event;
try {
// Your processing logic here
if (!req.card_number) {
return {
res: {
body: { error: "card_number is required" },
statusCode: 400
}
};
}
return {
res: {
body: { success: true },
statusCode: 200
}
};
} catch (error) {
logger.error("Processing failed", { error: error.message });
return {
res: {
body: { error: "Internal error" },
statusCode: 500
}
};
}
};
Best Practices
When integrating with an external API within a reactor, we strongly recommend handling all vendor-specific errors and translating them into appropriate error responses.
Some external APIs express error scenarios via HTTP status codes, while others opt to return a generic status code such as 200 OK while
expressing the error condition within the response body. Each API is unique and error handling may also look very different when using an
SDK vs making a direct HTTP call with a generic HTTP client. For these reasons, error handling logic needs to be
customized for each external API call made from within your reactor code.
In order to make it easier to debug errors returned by a reactor, we strongly encourage you to handle all potential errors and to craft your reactor responses such that you have sufficient information available in your system to react appropriately to each error scenario.
Examples
- node-bt
- node22
- Stripe
- Adyen
- Spreedly
module.exports = async function (req) {
const stripe = require('stripe')(req.configuration.STRIPE_PRIVATE_API_KEY);
const {
AuthenticationError,
BadRequestError,
InvalidPaymentMethodError,
RateLimitError,
ReactorRuntimeError,
} = require('@basis-theory/basis-theory-reactor-formulas-sdk-js');
try {
const token = await stripe.tokens.create({
// redacted for simplicity
});
return {
raw: token,
};
} catch (err) {
// the stripe sdk throws an error containing a type value
// here we translate from stripe-specific error types into Basis Theory errors
switch (err.type) {
case 'StripeCardError':
throw new InvalidPaymentMethodError();
case 'StripeRateLimitError':
throw new RateLimitError();
case 'StripeAuthenticationError':
throw new AuthenticationError();
case 'StripeInvalidRequestError':
throw new BadRequestError();
default:
throw new ReactorRuntimeError(err);
}
}
};
module.exports = async function (req) {
const {
AuthenticationError,
BadRequestError,
InvalidPaymentMethodError,
InvalidReactorConfigurationError,
RateLimitError,
ReactorRuntimeError,
} = require('@basis-theory/basis-theory-reactor-formulas-sdk-js');
const { Client, CheckoutAPI } = require('@adyen/api-library');
const { ADYEN_API_KEY, ADYEN_MERCHANT_ACCOUNT, ADYEN_ENVIRONMENT } =
req.configuration;
const client = new Client({
apiKey: ADYEN_API_KEY,
environment: ADYEN_ENVIRONMENT,
});
const checkout = new CheckoutAPI(client);
let res;
try {
res = await checkout.payments({
// redacted for simplicity
});
} catch (err) {
// Adyen's SDK throws an error that may contain an errorCode
switch (err.errorCode) {
case '101':
case '102':
case '103':
case '129':
case '140':
case '141':
case '153':
throw new InvalidPaymentMethodError(err);
case '159':
throw new RateLimitError(err);
case '180':
case '198':
throw new BadRequestError(err);
case '901':
throw new InvalidReactorConfigurationError(err);
}
// or the error may contain a statusCode
switch (err.statusCode) {
case 401:
case 403:
throw new AuthenticationError(err);
case 400:
case 422:
throw new BadRequestError(err);
case 429:
throw new RateLimitError(err);
default:
throw new ReactorRuntimeError(err);
}
}
// the API call may have also succeeded and returned an error resultCode
switch (res.resultCode) {
case 'Refused':
throw new InvalidPaymentMethodError(res);
case 'Error':
throw new ReactorRuntimeError(res);
default:
return {
raw: res,
};
}
};
module.exports = async function (req) {
const fetch = require('node-fetch');
const {
AuthenticationError,
BadRequestError,
InvalidPaymentMethodError,
RateLimitError,
ReactorRuntimeError,
} = require('@basis-theory/basis-theory-reactor-formulas-sdk-js');
const { SPREEDLY_API_ENV_KEY, SPREEDLY_API_ACCESS_KEY } = req.configuration;
const res = await fetch('https://core.spreedly.com/v1/payment_methods.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization:
`Basic ` +
Buffer.from(
`${SPREEDLY_API_ENV_KEY}:${SPREEDLY_API_ACCESS_KEY}`,
'binary'
).toString('base64'),
},
body: JSON.stringify(creationPayload),
});
if (res.status !== 201) {
const response = res.headers
.get('Content-Type')
?.includes('application/json')
? await res.json()
: await res.text();
// the spreedly api responds with an http status code indicating the error
// here we translate from a mixture of response status codes and errors in
// the response body into basis theory errors
switch (res.status) {
case 401:
case 403:
throw new AuthenticationError(response);
case 402:
case 422:
if (response.errors?.find((e) => e.key.startsWith('errors.metadata')))
throw new BadRequestError();
throw new InvalidPaymentMethodError(response);
case 429:
throw new RateLimitError(response);
default:
throw new ReactorRuntimeError(response);
}
}
return {
raw: await res.json(),
};
};
- Stripe
- Adyen
- Spreedly
module.exports = async function (event) {
const { req, configuration, logger } = event;
const stripe = require('stripe')(configuration.STRIPE_PRIVATE_API_KEY);
try {
const token = await stripe.tokens.create({
// redacted for simplicity
});
return {
res: {
body: token,
statusCode: 200
}
};
} catch (err) {
// the stripe sdk throws an error containing a type value
// here we translate from stripe-specific error types into appropriate error responses
switch (err.type) {
case 'StripeCardError':
return {
res: {
body: { error: "Invalid payment method" },
statusCode: 402
}
};
case 'StripeRateLimitError':
return {
res: {
body: { error: "Rate limit exceeded" },
statusCode: 429
}
};
case 'StripeAuthenticationError':
return {
res: {
body: { error: "Authentication failed" },
statusCode: 401
}
};
case 'StripeInvalidRequestError':
return {
res: {
body: { error: "Invalid request" },
statusCode: 400
}
};
default:
logger.error("Unexpected Stripe error", { error: err.message });
return {
res: {
body: { error: "Internal error" },
statusCode: 500
}
};
}
}
};
module.exports = async function (event) {
const { req, configuration, logger } = event;
const { Client, CheckoutAPI } = require('@adyen/api-library');
const { ADYEN_API_KEY, ADYEN_MERCHANT_ACCOUNT, ADYEN_ENVIRONMENT } =
configuration;
const client = new Client({
apiKey: ADYEN_API_KEY,
environment: ADYEN_ENVIRONMENT,
});
const checkout = new CheckoutAPI(client);
let res;
try {
res = await checkout.payments({
// redacted for simplicity
});
} catch (err) {
// Adyen's SDK throws an error that may contain an errorCode
switch (err.errorCode) {
case '101':
case '102':
case '103':
case '129':
case '140':
case '141':
case '153':
return {
res: {
body: { error: "Invalid payment method" },
statusCode: 402
}
};
case '159':
return {
res: {
body: { error: "Rate limit exceeded" },
statusCode: 429
}
};
case '180':
case '198':
return {
res: {
body: { error: "Bad request" },
statusCode: 400
}
};
case '901':
return {
res: {
body: { error: "Invalid configuration" },
statusCode: 400
}
};
}
// or the error may contain a statusCode
switch (err.statusCode) {
case 401:
case 403:
return {
res: {
body: { error: "Authentication failed" },
statusCode: 401
}
};
case 400:
case 422:
return {
res: {
body: { error: "Bad request" },
statusCode: 400
}
};
case 429:
return {
res: {
body: { error: "Rate limit exceeded" },
statusCode: 429
}
};
default:
logger.error("Unexpected Adyen error", { error: err.message });
return {
res: {
body: { error: "Internal error" },
statusCode: 500
}
};
}
}
// the API call may have also succeeded and returned an error resultCode
switch (res.resultCode) {
case 'Refused':
return {
res: {
body: { error: "Payment refused" },
statusCode: 402
}
};
case 'Error':
logger.error("Adyen API error", { resultCode: res.resultCode });
return {
res: {
body: { error: "Internal error" },
statusCode: 500
}
};
default:
return {
res: {
body: res,
statusCode: 200
}
};
}
};
module.exports = async function (event) {
const { req, configuration, logger } = event;
const fetch = require('node-fetch');
const { SPREEDLY_API_ENV_KEY, SPREEDLY_API_ACCESS_KEY } = configuration;
const res = await fetch('https://core.spreedly.com/v1/payment_methods.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization:
`Basic ` +
Buffer.from(
`${SPREEDLY_API_ENV_KEY}:${SPREEDLY_API_ACCESS_KEY}`,
'binary'
).toString('base64'),
},
body: JSON.stringify(creationPayload),
});
if (res.status !== 201) {
const response = res.headers
.get('Content-Type')
?.includes('application/json')
? await res.json()
: await res.text();
// the spreedly api responds with an http status code indicating the error
// here we translate from a mixture of response status codes and errors in
// the response body into appropriate error responses
switch (res.status) {
case 401:
case 403:
return {
res: {
body: { error: "Authentication failed" },
statusCode: 401
}
};
case 402:
case 422:
if (response.errors?.find((e) => e.key.startsWith('errors.metadata'))) {
return {
res: {
body: { error: "Bad request" },
statusCode: 400
}
};
}
return {
res: {
body: { error: "Invalid payment method" },
statusCode: 402
}
};
case 429:
return {
res: {
body: { error: "Rate limit exceeded" },
statusCode: 429
}
};
default:
logger.error("Unexpected Spreedly error", { status: res.status, response });
return {
res: {
body: { error: "Internal error" },
statusCode: 500
}
};
}
}
return {
res: {
body: await res.json(),
statusCode: 200
}
};
};