Skip to content

fix: surface userErrors and warnings from cart mutations in hydrogen-react#3679

Open
umxr wants to merge 4 commits intoShopify:mainfrom
umxr:fix/cart-mutations-surface-user-errors
Open

fix: surface userErrors and warnings from cart mutations in hydrogen-react#3679
umxr wants to merge 4 commits intoShopify:mainfrom
umxr:fix/cart-mutations-surface-user-errors

Conversation

@umxr
Copy link
Copy Markdown
Contributor

@umxr umxr commented Apr 9, 2026

WHY are these changes introduced?

Fixes #3378

All cart mutations in hydrogen-react silently discard userErrors and warnings returned by the Storefront API. The mutation queries only select the cart object from the response payload, meaning validation errors (e.g., invalid quantity) and warnings (e.g., discount not applicable) are lost. This makes it impossible for merchants to provide meaningful feedback to customers when a cart operation partially fails or produces warnings.

The hydrogen package already handles this correctly — each mutation query includes userErrors and warnings fields and surfaces them through formatAPIResult. This PR brings hydrogen-react in line with that behavior.

WHAT is this pull request doing?

  • cart-queries.ts — Adds userErrors { code field message } and warnings { code message target } to all 8 cart mutation queries (cartCreate, cartLinesAdd, cartLinesUpdate, cartLinesRemove, cartNoteUpdate, cartBuyerIdentityUpdate, cartAttributesUpdate, cartDiscountCodesUpdate)
  • cart-types.ts — Adds userErrors and warnings to CartMachineContext, CartWithActions, and the RESOLVE event payload
  • useCartActions.tsx — Updates fetch type generics to include userErrors and warnings in mutation response types
  • useCartAPIStateMachine.tsx — Extracts userErrors/warnings from each mutation response, passes them through the state machine, and clears them on error/completion transitions
  • CartProvider.tsx — Exposes userErrors and warnings on the useCart() return value

After this change, merchants can access errors and warnings like so:

const { userErrors, warnings } = useCart();

HOW to test your changes?

  1. pnpm install
  2. Run the existing + new tests: cd packages/hydrogen-react && npx vitest run src/CartProvider.test.tsx
  3. All 60 tests should pass, including 3 new ones:
    • surfaces userErrors from the mutation response
    • surfaces warnings from the mutation response
    • clears userErrors and warnings on subsequent successful mutation
  4. Type check: npx tsc --noEmit --project packages/hydrogen-react/tsconfig.json — no new errors

Checklist

  • I've read the Contributing Guidelines
  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've added a changeset if this PR contains user-facing or functional changes. Test changes or internal-only config changes do not require a changeset.
  • I've added tests to cover my changes
  • I've added or updated the documentation

umxr added 2 commits April 10, 2026 00:25
…react

Cart mutations in hydrogen-react were silently discarding userErrors and
warnings returned by the Storefront API. This made it impossible for
merchants to display validation errors or warnings to customers.

Closes Shopify#3378
Verifies that:
- userErrors from mutation responses are surfaced via useCart()
- warnings from mutation responses are surfaced via useCart()
- userErrors and warnings are cleared on subsequent successful mutations
@umxr umxr requested a review from a team as a code owner April 9, 2026 23:26
cartLineAdd: cartLineAddSpy,
});

void act(() => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: all three new tests are missing "before" assertions for userErrors and warnings.

The tests assert the final state after the mutation, but don't verify the initial state before. A "before" assertion confirms the mutation is what causes the state change, not some pre-existing value.

let's add assertions before each void act() call:

// Before linesAdd
expect(result.current.userErrors).toBeUndefined();
expect(result.current.warnings).toBeUndefined();

void act(() => {
  result.current.linesAdd([{merchandiseId: '123'}]);
});

Same applies to the "clears userErrors and warnings" test - the initial state before the first mutation should be verified too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in fc0d51d

cart {
...CartFragment
}
userErrors {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking: the userErrors and warnings field selections are copy-pasted into all 8 mutations (80 lines of duplication).

The hydrogen package avoids this with GraphQL fragments (USER_ERROR_FRAGMENT, CART_WARNING_FRAGMENT in packages/hydrogen/src/cart/queries/cart-fragments.ts). If the Storefront API ever adds a field to CartUserError or CartWarning, all 8 mutations would need updating in lockstep - classic change amplification.

A simple extraction would eliminate this:

const CART_MUTATION_FIELDS = `
  userErrors { code field message }
  warnings { code message target }
`;

Then reference via ${CART_MUTATION_FIELDS} in each mutation. The existing hydrogen-react queries don't use fragments so this is consistent with the current pattern, but the duplication is substantial enough to warrant extraction.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in f8b0f9c

>,
): CartMachineFetchResultEvent {
if (errors) {
return {type: 'ERROR', payload: {errors, cartActionEvent}};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking: when GraphQL-level errors exist, userErrors and warnings from the same response are silently discarded here.

The CartMachineFetchResultEvent ERROR case doesn't include these fields in its type either. If a mutation response contains both GraphQL errors and userErrors, the userErrors are lost.

This follows the pre-existing pattern for how errors takes priority over everything else, so it's consistent. But worth noting as a known behavioural limitation - a Storefront API response can technically contain both.

cartActionEvent: CartMachineActionEvent,
cart?: PartialDeep<CartType, {recurseIntoArrays: true}> | null,
errors?: unknown,
userErrors?: Array<
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking: eventFromFetchResult now takes 5 positional params with a type asymmetry - these last two accept Array<PartialDeep<CartUserError> | undefined> and filter internally to CartUserError[]. The PartialDeep wrapper from the generic fetchCart return type leaks into the function interface.

The internal narrowing is correct (pull complexity downward), but the interface exposes an implementation detail of the data pipeline. Not worth changing in this PR since it follows the existing pattern, but something to keep in mind for future work.

prevCart: (context) => context?.lastValidCart,
cart: (context) => context?.lastValidCart,
errors: (_, event) => event?.payload?.errors,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: these eslint-disable-next-line comments follow the existing pattern in the file (same comments exist for errors and rawCartResult clearing). Pre-existing pattern issue, not introduced by this PR.


type CartResponse = PartialDeep<CartType, {recurseIntoArrays: true}>;

type CartMutationResponse = {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: nice - the introduction of CartMutationResponse centralises the mutation response shape, makes it impossible to forget userErrors or warnings in a new mutation, and reduces cognitive load across the 8 callbacks.

cart: cartFromGraphQL(cart),
rawCartResult: cart,
cartActionEvent,
userErrors: userErrors?.filter((e): e is CartUserError => e != null),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: good use of CartUserError and CartWarning from storefront-api-types rather than defining custom types. The type guard filter here is a proper narrowing pattern - nice.

@umxr umxr requested a review from fredericoo April 13, 2026 17:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hydrogen-react cart mutations swallow userErrors and warnings

2 participants