hcengineering/platform · error · PaymentError

${error.error ?? text}

Error message

${error.error ?? text}

What it means

When the payment service returns a non-OK HTTP status and its body parses as JSON, fetchSafe throws a PaymentError carrying the server's `error` field (falling back to the raw body text). This surfaces the backend's own explanation of the failure — declined payment, invalid request, auth rejection, etc. — to the caller as a typed error.

Source

Thrown at packages/payment-client/src/client.ts:168

 * @param url - URL to fetch
 * @param init - Fetch options
 * @returns Response
 * @throws NetworkError on network issues
 * @throws PaymentError on non-ok responses
 */
async function fetchSafe (url: string | URL, init?: RequestInit): Promise<Response> {
  let response
  try {
    response = await fetch(url, init)
  } catch (err: any) {
    throw new NetworkError(`Network error: ${String(err)}`)
  }

  if (!response.ok) {
    const text = await response.text()
    try {
      const error = JSON.parse(text)
      throw new PaymentError(error.error ?? text)
    } catch {
      throw new PaymentError(`Payment service error: ${response.status} ${text}`)
    }
  }

  return response
}

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Read the PaymentError message — it contains the server's error description identifying the exact issue.
  2. Fix the request payload according to the server message (validation errors, missing fields).
  3. Refresh/replace the auth token if the error indicates authentication failure.
  4. Handle PaymentError distinctly in try/catch to branch on business failures (declined vs. transient).

Example fix

// before
const res = await paymentClient.response('/charge', init)
// after
try {
  const res = await paymentClient.response('/charge', init)
} catch (e) {
  if (e instanceof PaymentError) {
    console.error('Payment failed:', e.message) // e.g. 'card declined'
  } else throw e
}
Defensive patterns

Strategy: try-catch

Validate before calling

// validate request body before calling to avoid predictable 400s
function assertChargeRequest(req: { amount: number; currency: string }): void {
  if (!(req.amount > 0)) throw new Error('amount must be positive')
  if (!/^[A-Z]{3}$/.test(req.currency)) throw new Error('invalid currency code')
}

Type guard

function isPaymentError(e: unknown): e is PaymentError {
  return e instanceof PaymentError
}

Try / catch

try {
  await paymentClient.response('/charge', init)
} catch (e) {
  if (isPaymentError(e)) {
    const reason = e.message // server-provided error, e.g. 'card declined'
    // branch on business failure; do not blindly retry
  } else throw e
}

Prevention

When it happens

Trigger: Any PaymentClient API call (via `response` → fetchSafe) receiving a 4xx/5xx response whose body is valid JSON; PaymentError(message of `error.error ?? text`) is thrown. Note: if `error.error` is itself falsy, the raw body text is used.

Common situations: Invalid or expired token (401/403), malformed charge request (400/422), insufficient funds or declined transaction from the upstream payment provider, or rate limiting (429).

Related errors


AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/7dd4f581d4bba35d. Report an issue: GitHub.