Skip to main content

Error Handling Strategies

This guide covers error handling patterns and best practices for SolvaPay SDK across different frameworks and use cases.

Table of Contents

Error Types

SolvaPay SDK uses several error types:

PaywallError

Thrown when a paywall is triggered (subscription required or usage limit exceeded):

import { PaywallError } from '@solvapay/server';

class PaywallError extends Error {
message: string;
structuredContent: PaywallStructuredContent;
}

SolvaPayError

Base error class for SolvaPay SDK errors:

import { SolvaPayError } from '@solvapay/core';

class SolvaPayError extends Error {
// Base error for SDK errors
}

Standard Errors

Regular JavaScript errors from your business logic or network issues.

PaywallError Handling

Understanding PaywallError

PaywallError is thrown when:

  • Customer doesn't have required subscription
  • Customer has exceeded usage limits
  • Customer needs to upgrade their plan

It includes structured content with checkout URLs and metadata:

interface PaywallStructuredContent {
kind: 'payment_required';
agent: string;
checkoutUrl: string;
message: string;
plan?: string;
remaining?: number;
}

Basic Handling

import { PaywallError } from '@solvapay/server';

try {
const result = await payable.http(createTask)(req, res);
return result;
} catch (error) {
if (error instanceof PaywallError) {
// Handle paywall error
return res.status(402).json({
error: 'Payment required',
checkoutUrl: error.structuredContent.checkoutUrl,
message: error.structuredContent.message,
});
}

// Handle other errors
throw error;
}

Framework-Specific Patterns

Express.js

Basic Error Handling

import express from 'express';
import { PaywallError } from '@solvapay/server';

const app = express();

// Protected route
app.post('/api/tasks', payable.http(createTask));

// Global error handler
app.use((error: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
if (error instanceof PaywallError) {
return res.status(402).json({
error: 'Payment required',
checkoutUrl: error.structuredContent.checkoutUrl,
agent: error.structuredContent.agent,
message: error.structuredContent.message,
});
}

// Log other errors
console.error('Unhandled error:', error);
res.status(500).json({ error: 'Internal server error' });
});

Custom Error Middleware

function paywallErrorHandler(
error: Error,
req: express.Request,
res: express.Response,
next: express.NextFunction
) {
if (error instanceof PaywallError) {
// Custom paywall response
return res.status(402).json({
success: false,
error: {
type: 'paywall',
message: error.message,
checkoutUrl: error.structuredContent.checkoutUrl,
agent: error.structuredContent.agent,
plan: error.structuredContent.plan,
remaining: error.structuredContent.remaining,
},
});
}

next(error);
}

app.use(paywallErrorHandler);

Per-Route Error Handling

async function handleWithErrorHandling(
req: express.Request,
res: express.Response,
next: express.NextFunction
) {
try {
const handler = payable.http(createTask);
await handler(req, res);
} catch (error) {
if (error instanceof PaywallError) {
return res.status(402).json({
error: 'Payment required',
checkoutUrl: error.structuredContent.checkoutUrl,
});
}
next(error);
}
}

app.post('/api/tasks', handleWithErrorHandling);

Next.js

API Route Error Handling

import { NextRequest, NextResponse } from 'next/server';
import { PaywallError } from '@solvapay/server';

export async function POST(request: NextRequest) {
try {
const handler = payable.next(createTask);
return await handler(request);
} catch (error) {
if (error instanceof PaywallError) {
return NextResponse.json(
{
error: 'Payment required',
checkoutUrl: error.structuredContent.checkoutUrl,
message: error.structuredContent.message,
},
{ status: 402 }
);
}

console.error('Unhandled error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

Using Helper Functions

import { NextRequest, NextResponse } from 'next/server';
import { checkSubscription } from '@solvapay/next';
import { isErrorResult, handleRouteError } from '@solvapay/server';

export async function GET(request: NextRequest) {
const result = await checkSubscription(request);

// Check if result is an error
if (isErrorResult(result)) {
return NextResponse.json(result, { status: result.status || 500 });
}

return NextResponse.json(result);
}

Error Handling Utility

// lib/error-handler.ts
import { NextRequest, NextResponse } from 'next/server';
import { PaywallError } from '@solvapay/server';

export function handleApiError(error: unknown, request: NextRequest) {
if (error instanceof PaywallError) {
return NextResponse.json(
{
error: 'Payment required',
checkoutUrl: error.structuredContent.checkoutUrl,
agent: error.structuredContent.agent,
message: error.structuredContent.message,
},
{ status: 402 }
);
}

if (error instanceof Error) {
console.error('API Error:', error);
return NextResponse.json(
{ error: error.message },
{ status: 500 }
);
}

return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}

// Usage
export async function POST(request: NextRequest) {
try {
const handler = payable.next(createTask);
return await handler(request);
} catch (error) {
return handleApiError(error, request);
}
}

React (Client-Side)

Component Error Handling

import { PaymentForm } from '@solvapay/react';
import { useState } from 'react';

function CheckoutPage() {
const [error, setError] = useState<string | null>(null);

return (
<div>
{error && (
<div className="error-message">
{error}
</div>
)}
<PaymentForm
planRef="pln_premium"
agentRef="agt_myapi"
onSuccess={() => {
setError(null);
// Handle success
}}
onError={(error) => {
setError(error.message || 'Payment failed');
}}
/>
</div>
);
}

Hook Error Handling

import { useCheckout } from '@solvapay/react';

function CustomCheckout() {
const { createPayment, processPayment, error, isLoading } = useCheckout(
'pln_premium',
'agt_myapi'
);

const handleCheckout = async () => {
try {
const intent = await createPayment();
const result = await processPayment(intent.paymentIntentId);

if (!result.success) {
throw new Error(result.error || 'Payment failed');
}
} catch (error) {
console.error('Checkout error:', error);
// Handle error
}
};

return (
<div>
{error && <div>Error: {error.message}</div>}
<button onClick={handleCheckout} disabled={isLoading}>
Checkout
</button>
</div>
);
}

MCP Server

Tool Error Handling

import { PaywallError } from '@solvapay/server';
import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';

server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;

try {
switch (name) {
case 'create_task': {
const handler = payable.mcp(createTask);
const result = await handler(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result),
},
],
};
}

default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
if (error instanceof PaywallError) {
// Return paywall error in MCP format
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'Payment required',
message: error.message,
checkoutUrl: error.structuredContent.checkoutUrl,
agent: error.structuredContent.agent,
plan: error.structuredContent.plan,
}),
},
],
isError: true,
};
}

// Handle other errors
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: error instanceof Error ? error.message : 'Unknown error',
}),
},
],
isError: true,
};
}
});

Best Practices

1. Always Check Error Type

try {
// Your code
} catch (error) {
if (error instanceof PaywallError) {
// Handle paywall error
} else if (error instanceof SolvaPayError) {
// Handle SDK error
} else {
// Handle other errors
}
}

2. Provide User-Friendly Messages

if (error instanceof PaywallError) {
return res.status(402).json({
error: 'Subscription required',
message: 'Please subscribe to access this feature.',
checkoutUrl: error.structuredContent.checkoutUrl,
});
}

3. Log Errors Appropriately

catch (error) {
if (error instanceof PaywallError) {
// Don't log paywall errors as they're expected
// Just return the error response
} else {
// Log unexpected errors
console.error('Unexpected error:', error);
// Send generic error to client
}
}

4. Use Error Boundaries (React)

import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }) {
if (error.message.includes('Payment')) {
return (
<div>
<h2>Payment Required</h2>
<p>Please subscribe to access this feature.</p>
<button onClick={resetErrorBoundary}>Try Again</button>
</div>
);
}

return (
<div>
<h2>Something went wrong</h2>
<button onClick={resetErrorBoundary}>Try Again</button>
</div>
);
}

function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<YourApp />
</ErrorBoundary>
);
}

5. Retry Logic for Transient Errors

import { withRetry } from '@solvapay/server';

async function createTaskWithRetry(req: express.Request) {
return withRetry(
async () => {
const handler = payable.http(createTask);
return await handler(req, res);
},
{
maxRetries: 3,
retryDelay: 1000,
shouldRetry: (error) => {
// Don't retry paywall errors
if (error instanceof PaywallError) {
return false;
}
// Retry network errors
return error instanceof NetworkError;
},
}
);
}

Complete Examples

Express.js Complete Example

import express from 'express';
import { createSolvaPay, PaywallError } from '@solvapay/server';

const app = express();
app.use(express.json());

const solvaPay = createSolvaPay({ apiKey: process.env.SOLVAPAY_SECRET_KEY });
const payable = solvaPay.payable({ agent: 'agt_myapi', plan: 'pln_premium' });

// Protected route
app.post('/api/tasks', payable.http(createTask));

// Error handling middleware
app.use((error: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
if (error instanceof PaywallError) {
return res.status(402).json({
success: false,
error: {
type: 'paywall',
message: error.message,
checkoutUrl: error.structuredContent.checkoutUrl,
agent: error.structuredContent.agent,
plan: error.structuredContent.plan,
},
});
}

// Log unexpected errors
console.error('Unhandled error:', {
message: error.message,
stack: error.stack,
path: req.path,
method: req.method,
});

// Don't expose internal errors to client
res.status(500).json({
success: false,
error: {
type: 'internal',
message: 'An internal error occurred',
},
});
});

app.listen(3000);

Next.js Complete Example

// app/api/tasks/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createSolvaPay, PaywallError } from '@solvapay/server';

const solvaPay = createSolvaPay({ apiKey: process.env.SOLVAPAY_SECRET_KEY });
const payable = solvaPay.payable({ agent: 'agt_myapi', plan: 'pln_premium' });

async function createTask(req: NextRequest) {
const body = await req.json();
return { success: true, task: { title: body.title } };
}

export async function POST(request: NextRequest) {
try {
const handler = payable.next(createTask);
return await handler(request);
} catch (error) {
if (error instanceof PaywallError) {
return NextResponse.json(
{
success: false,
error: {
type: 'paywall',
message: error.message,
checkoutUrl: error.structuredContent.checkoutUrl,
agent: error.structuredContent.agent,
},
},
{ status: 402 }
);
}

console.error('API Error:', error);
return NextResponse.json(
{
success: false,
error: {
type: 'internal',
message: 'An internal error occurred',
},
},
{ status: 500 }
);
}
}

Next Steps