Skip to content

Errors

Reference for the three error classes, the guards, and the problem document. For how to use them, see Errors in the guide.

ts
import {
  MawjodApiError,
  MawjodNetworkError,
  PayloadIntegrityError,
  isMawjodApiError,
  isMawjodNetworkError,
  isPayloadIntegrityError,
  isValidationError,
  isUnauthenticated,
  isForbidden,
  isStoreUnavailable,
  isCheckoutError,
  isStaleCartError,
  type CheckoutErrorCode,
  type MawjodErrorCode,
  type ProblemDocument,
  type StaleCartErrorCode,
} from '@mawjod/api'

MawjodApiError

A problem+json failure returned by the API.

ts
class MawjodApiError extends Error {
  readonly name: 'MawjodApiError'
  readonly status: number
  readonly code: MawjodErrorCode
  readonly title: string | undefined
  readonly detail: string | undefined
  readonly requestId: string | undefined
  readonly errors: Record<string, string[]> | undefined
  readonly problem: ProblemDocument
}
PropertyNotes
statusTaken from the problem document, which always mirrors the HTTP status
codeThe machine-readable key. The only field worth branching on.
titleA human title
detailProse for a person. Reworded without notice; never parse it.
requestIdMatches the X-Request-ID response header. Quote it in bug reports.
errorsField errors, present on 422
problemThe untouched document, for codes the SDK does not model

message is "<code> (<status>)", or "<code> (<status>): <title>" when a title is present.

MawjodNetworkError

A failure that never reached the problem+json contract.

ts
class MawjodNetworkError extends Error {
  readonly name: 'MawjodNetworkError'
  readonly url: string | undefined
  readonly status: number | undefined
}

Thrown when:

  • the request could not be sent at all (cause carries the underlying failure)
  • the response body could not be read
  • a 2xx body is not JSON
  • a non-2xx response carries no code (a proxy page, a gateway, an unhandled server fault)
  • a success envelope is missing its data
  • /sanctum/csrf-cookie answers anything other than 204

That last one matters. The CSRF route sits outside /api/v1, does not speak problem+json, and carries no code or request_id. Retry the CSRF call, not the write that triggered it.

PayloadIntegrityError

A well-formed 200 that cannot be true.

ts
class PayloadIntegrityError extends Error {
  readonly name: 'PayloadIntegrityError'
  readonly resource: PayloadIntegrityResource   // 'order' | 'return' | 'search_hit'
  readonly resourceId: string | null
  readonly requestId: string | undefined
}

An order or a return is created from at least one line, so lines: [] is a lost payload rather than an empty state. Thrown by orders.list, orders.get, orders.cancel, checkout.place, returns.list, returns.get, returns.create and returns.cancel.

A search hit is only useful because it can be followed, and slug is the whole address, so slug: '' is a lost projection rather than a product without an address. Thrown by search.products with resource: 'search_hit' and the hit's id as resourceId.

On a list or a results page, one bad row throws for the whole page.

Type guards

GuardNarrows toMatches
isMawjodApiError(e)MawjodApiErrorany problem+json failure
isMawjodNetworkError(e)MawjodNetworkErrorany transport failure
isPayloadIntegrityError(e)PayloadIntegrityErrorthe empty-lines and empty-slug guards
isValidationError(e)MawjodApiErrorcode === 'validation_failed'
isUnauthenticated(e)MawjodApiErrorcode === 'unauthenticated'
isForbidden(e)MawjodApiErrorcode === 'forbidden'
isStoreUnavailable(e)MawjodApiErrorcode === 'store_unavailable'
isCheckoutError(e)MawjodApiError & { code: CheckoutErrorCode }the seven checkout codes
isStaleCartError(e)MawjodApiError & { code: StaleCartErrorCode }the three stale-cart codes

ProblemDocument

ts
interface ProblemDocument {
  type?: string
  title?: string
  status: number
  detail?: string
  instance?: string
  code: string
  request_id?: string
  errors?: Record<string, string[]>   // present on 422
  reason?: string                     // present on some 409s, e.g. pricing_conflict -> 'expired'
  checks?: Record<string, boolean>    // present on deployment_not_ready
  [key: string]: unknown
}

The index signature is there because the API may add fields; reach through error.problem for anything the class does not surface.

Code types

ts
type StaleCartErrorCode =
  | 'cart_price_changed'
  | 'cart_not_purchasable'
  | 'insufficient_stock'

type CheckoutErrorCode =
  | StaleCartErrorCode
  | 'cart_empty'
  | 'cart_not_found'
  | 'payment_method_unavailable'
  | 'customer_not_verified'

type MawjodErrorCode =
  | 'unauthenticated'
  | 'forbidden'
  | 'validation_failed'
  | 'store_unavailable'
  | 'rate_limited'
  | 'not_found'
  | 'untrusted_host'
  | CheckoutErrorCode
  | 'variant_not_purchasable'
  | 'pricing_conflict'
  | 'outside_service_area'
  | 'identity_unavailable'
  | 'invalid_identity_challenge'
  | 'cancellation_window_closed'
  | 'payment_already_resolved'
  | 'payment_provider_unavailable'
  | 'return_window_closed'
  | 'return_transition_not_allowed'
  | 'evidence_not_an_image'
  | 'banner_has_no_image'
  | 'slide_has_no_image'
  | 'banner_location_in_use'
  | 'search_unavailable'
  | 'deployment_not_ready'
  | (string & {})

customer_not_verified is conditional. It arrives only from a store that has turned on auth.customer_verification_required, which is off by default. See store.settings() → Verification.

untrusted_host is a 400 refused before the endpoint ran: the request's Host header is not in the deployment's TRUSTED_HOSTS. It is a deployment or proxy misconfiguration rather than anything a shopper did, so it is worth one distinct screen. Nothing a theme retries will clear it.

The three content codes are staff-write refusals: banner_has_no_image and slide_has_no_image when a banner or slide is taken live without a stored public image, and banner_location_in_use when a location is deleted while banners still reference it. The storefront surface cannot produce them. They are in the union because code is one vocabulary across the whole API, and a theme that shares an error renderer with a staff tool should not fall through to a generic message.

MawjodErrorCode is deliberately open. The (string & {}) member keeps autocomplete for the known codes while letting an unrecognized one typecheck: the server may introduce a code at any time, and a client that crashes on one is worse than a client that falls through to a generic message.

onError

Every problem+json failure passes through the client's onError callback before it is thrown. It does not swallow the error.

ts
createMawjodClient({
  baseUrl,
  onError: (error) => {
    if (isStoreUnavailable(error)) {
      shopPaused.value = true
    }
  },
})

MawjodNetworkError and PayloadIntegrityError do not go through onError; it is typed for MawjodApiError only.

Status quick reference

StatusMeaning here
400untrusted_host (the Host header is not in TRUSTED_HOSTS)
401unauthenticated (no session, or it expired)
403forbidden, and customer_not_verified on a store that requires verification
404not_found
409The world moved, or a window closed. Refetch.
419CSRF mismatch. Handled internally: refresh once, replay once.
422Validation, or a named refusal like outside_service_area
429rate_limited
503store_unavailable, search_unavailable, payment_provider_unavailable, deployment_not_ready

You will never see a 419 as a thrown error unless a second one arrives after the retry, at which point it is a real failure and not a race.