Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix async error stack in Storefront and Customer Account clients #1656

Merged
merged 5 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-starfishes-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/hydrogen': patch
---

Fix error stack traces thrown from Storefront API and Customer Account API clients when promises are not awaited.
10 changes: 7 additions & 3 deletions packages/hydrogen/src/customer/customer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
import {parseJSON} from '../utils/parse-json';
import {hashKey} from '../utils/hash';
import {CrossRuntimeRequest, getDebugHeaders} from '../utils/request';
import {getCallerStackLine} from '../utils/callsites';
import {getCallerStackLine, withSyncStack} from '../utils/callsites';

type CustomerAPIResponse<ReturnType> = {
data: ReturnType;
Expand Down Expand Up @@ -355,13 +355,17 @@ export function createCustomerClient({
mutation = minifyQuery(mutation);
assertMutation(mutation, 'customer.mutate');

return fetchCustomerAPI({query: mutation, type: 'mutation', ...options});
return withSyncStack(
fetchCustomerAPI({query: mutation, type: 'mutation', ...options}),
);
},
query(query, options?) {
query = minifyQuery(query);
assertQuery(query, 'customer.query');

return fetchCustomerAPI({query, type: 'query', ...options});
return withSyncStack(
fetchCustomerAPI({query, type: 'query', ...options}),
);
},
authorize: async () => {
const code = url.searchParams.get('code');
Expand Down
24 changes: 3 additions & 21 deletions packages/hydrogen/src/storefront.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import {
type GraphQLApiResponse,
type GraphQLErrorOptions,
} from './utils/graphql';
import {getCallerStackLine} from './utils/callsites';
import {getCallerStackLine, withSyncStack} from './utils/callsites';

export type I18nBase = {
language: LanguageCode;
Expand Down Expand Up @@ -399,16 +399,7 @@ export function createStorefrontClient<TI18n extends I18nBase>(
query = minifyQuery(query);
assertQuery(query, 'storefront.query');

const result = fetchStorefrontApi({
...options,
query,
});

// This is a no-op, but we need to catch the promise to avoid unhandled rejections
// we cannot return the catch no-op, or it would swallow the error
result.catch(() => {});

return result;
return withSyncStack(fetchStorefrontApi({...options, query}));
},
/**
* Sends a GraphQL mutation to the Storefront API.
Expand All @@ -427,16 +418,7 @@ export function createStorefrontClient<TI18n extends I18nBase>(
mutation = minifyQuery(mutation);
assertMutation(mutation, 'storefront.mutate');

const result = fetchStorefrontApi({
...options,
mutation,
});

// This is a no-op, but we need to catch the promise to avoid unhandled rejections
// we cannot return the catch no-op, or it would swallow the error
result.catch(() => {});

return result;
return withSyncStack(fetchStorefrontApi({...options, mutation}));
},
cache,
CacheNone,
Expand Down
17 changes: 17 additions & 0 deletions packages/hydrogen/src/utils/callsites.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
/**
* Ensures that the error of an async rejected promise
* contains the entire synchronous stack trace.
*/
export function withSyncStack<T>(promise: Promise<T>): Promise<T> {
const syncError = new Error();

return promise.catch((error: Error) => {
// Remove error message, caller function and current function from the stack.
const syncStack = (syncError.stack ?? '').split('\n').slice(3).join('\n');

error.stack = `Error: ${error.message}\n` + syncStack;

throw error;
});
}

export type StackInfo = {
file?: string;
func?: string;
Expand Down
Loading