Skip to content

Error Handling

@yildizpay/http-adapter converts every raw error — HTTP failures, OS-level network errors, and unexpected exceptions — into a structured, typed exception class. Your catch blocks never need to inspect raw status codes or error codes manually.

Exception Hierarchy

BaseAdapterException
├── HttpException                    (any HTTP response error)
│   ├── BadRequestException          (400)
│   ├── UnauthorizedException        (401)
│   ├── ForbiddenException           (403)
│   ├── NotFoundException            (404)
│   ├── ConflictException            (409)
│   ├── UnprocessableEntityException (422)
│   ├── TooManyRequestsException     (429)  ← isRetryable() = true
│   ├── InternalServerErrorException (500)
│   ├── BadGatewayException          (502)  ← isRetryable() = true
│   ├── ServiceUnavailableException  (503)  ← isRetryable() = true
│   ├── GatewayTimeoutException      (504)  ← isRetryable() = true
│   └── ... (all 4xx / 5xx codes)
├── NetworkException                 (OS-level connectivity failures)
│   ├── ConnectionRefusedException   (ECONNREFUSED)  ← isRetryable() = true
│   ├── TimeoutException             (ETIMEDOUT / ECONNABORTED / AbortError)  ← isRetryable() = true
│   ├── SocketResetException         (ECONNRESET)  ← isRetryable() = true
│   ├── DnsResolutionException       (ENOTFOUND / EAI_AGAIN)
│   └── HostUnreachableException     (EHOSTUNREACH / ENETUNREACH)
├── ValidationException              (response failed a ResponseValidator)
├── UnknownException                 (any unclassifiable error)
└── CircuitBreakerOpenException      (circuit is open, request not sent)

Catching Exceptions by Type

typescript
import {
  NotFoundException,
  TooManyRequestsException,
  TimeoutException,
  ConnectionRefusedException,
  CircuitBreakerOpenException,
  UnknownException,
} from '@yildizpay/http-adapter';

try {
  const response = await adapter.send<PaymentResponse>(request);
} catch (error) {
  if (error instanceof NotFoundException) {
    // HTTP 404
    console.error('Resource not found:', error.response.data);

  } else if (error instanceof TooManyRequestsException) {
    // HTTP 429 — use the Retry-After header value if present
    const retryAfterMs = error.getRetryAfterMs();
    console.warn(`Rate limited. Retry after ${retryAfterMs}ms`);

  } else if (error instanceof TimeoutException) {
    // ETIMEDOUT / AbortError — downstream service too slow
    console.error('Request timed out:', error.code);

  } else if (error instanceof ConnectionRefusedException) {
    // ECONNREFUSED — downstream service is down
    console.error('Service is down:', error.requestContext?.url);

  } else if (error instanceof CircuitBreakerOpenException) {
    // Circuit is open — fail fast without hitting the server
    console.error('Circuit breaker is open. Not sending request.');

  } else if (error instanceof UnknownException) {
    console.error('Unhandled error:', error.toJSON());
  }
}

Type Guards

If you prefer narrowing without instanceof — useful in functional pipelines or when crossing module boundaries — every exception class has a corresponding type guard:

typescript
import {
  isHttpException,
  isTimeoutException,
  isConnectionRefusedException,
  isCircuitBreakerOpenException,
  isNetworkException,
} from '@yildizpay/http-adapter';

function handleError(error: unknown): void {
  if (isTimeoutException(error)) {
    // TypeScript now knows: error is TimeoutException
    scheduleRetry(error.requestContext?.url);
  } else if (isHttpException(error)) {
    // TypeScript now knows: error is HttpException
    reportToMonitoring(error.response.status, error.response.data);
  }
}

isRetryable() Signal

Every exception exposes isRetryable(): boolean — useful when implementing custom retry decorators or deciding at the application layer whether to propagate or retry:

typescript
} catch (error) {
  if (error instanceof BaseAdapterException && error.isRetryable()) {
    return retryOperation();
  }
  throw error;
}

Retryable exceptions: TooManyRequestsException (429), BadGatewayException (502), ServiceUnavailableException (503), GatewayTimeoutException (504), TimeoutException, SocketResetException, ConnectionRefusedException.

Structured Logging with toJSON()

All exceptions override toJSON(), making them compatible with structured loggers (Pino, Winston, etc.). JSON.stringify(error) produces a complete, nested object — not the empty {} you'd get from a plain Error:

typescript
} catch (error) {
  if (error instanceof BaseAdapterException) {
    logger.error(error.toJSON());
    // {
    //   name: 'NotFoundException',
    //   message: 'Not Found',
    //   code: 'ERR_NOT_FOUND',
    //   stack: '...',
    //   response: {
    //     status: 404,
    //     data: { detail: 'Payment record not found' },
    //     request: {
    //       method: 'GET',
    //       url: 'https://api.example.com/payments/123',
    //       correlationId: 'corr-abc'
    //     }
    //   }
    // }
  }
}

RequestContext — Safe Request Metadata

Every exception carries a RequestContext object with method, url, and correlationId sourced from the originating request. Headers and body are deliberately excluded to prevent accidental auth-token or PII leakage in logs.

typescript
} catch (error) {
  if (error instanceof NetworkException) {
    logger.warn({
      event: 'network_failure',
      exception: error.name,
      request: error.requestContext, // { method, url, correlationId }
    });
  }
}

Response Validators

Attach validators to a request to enforce schema constraints or business rules on the response before it reaches your code. Validators run sequentially after the HTTP call succeeds. The first validator that throws halts the chain.

typescript
import { ResponseValidator, ValidationException, Response } from '@yildizpay/http-adapter';

class PaymentStatusValidator implements ResponseValidator<IyzicoResponse> {
  validate(response: Response<IyzicoResponse>): void {
    if (response.data.status !== 'success') {
      throw new ValidationException(
        `Payment failed: ${response.data.errorMessage}`,
        response,
      );
    }
  }
}

// Works with any schema validation library — zero coupling to Zod, Joi, etc.
class PaymentSchemaValidator implements ResponseValidator<unknown> {
  validate(response: Response<unknown>): void {
    IyzicoResponseSchema.parse(response.data); // Zod throws on mismatch
  }
}

const request = new RequestBuilder('https://api.iyzipay.com')
  .setEndpoint('/payment/auth')
  .setMethod(HttpMethod.POST)
  .setBody(dto)
  .validateWith(new PaymentSchemaValidator(), new PaymentStatusValidator())
  .build();

Catching a validation failure:

typescript
import { isValidationException } from '@yildizpay/http-adapter';

} catch (error) {
  if (isValidationException(error)) {
    console.error('Validation failed:', error.message);
    console.error('Raw response:', error.response.data);
  }
}

Non-BaseAdapterException errors thrown inside a validator (e.g., ZodError) are automatically wrapped in ValidationException with the original error available as error.cause:

typescript
} catch (error) {
  if (isValidationException<ZodError>(error) && error.cause) {
    console.error('Schema issues:', error.cause.issues);
  }
}

Released under the MIT License.