Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

fix: Don't obscure errors from inner wrapped components. #2133

Merged
merged 9 commits into from
Jul 17, 2018
7 changes: 7 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Change log

## vNext

- Fixed an issue in `getDataFromTree` where queries that threw more than one
error had error messages swallowed, and returned an invalid error object
with circular references. Multiple errors are now preserved.
[@anand-sundaram-zocdoc](https://github.com/anand-sundaram-zocdoc) in [#2133](https://github.com/apollographql/react-apollo/pull/2133)

## 2.1.9 (July 4, 2018)

- Added `onCompleted` and `onError` props to the `Query` component, than can
Expand Down
49 changes: 32 additions & 17 deletions src/getDataFromTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,34 +205,49 @@ function getPromisesFromTree({
return promises;
}

export default function getDataFromTree(
function getDataAndErrorsFromTree(
rootElement: React.ReactNode,
rootContext: any = {},
storeError: Function,
): Promise<any> {
const promises = getPromisesFromTree({ rootElement, rootContext });

if (!promises.length) {
return Promise.resolve();
}

const errors: any[] = [];

const mappedPromises = promises.map(({ promise, context, instance }) => {
return promise
.then(_ => getDataFromTree(instance.render(), context))
.catch(e => errors.push(e));
.then(_ => getDataAndErrorsFromTree(instance.render(), context, storeError))
.catch(e => storeError(e));
});

return Promise.all(mappedPromises).then(_ => {
if (errors.length > 0) {
const error =
errors.length === 1
? errors[0]
: new Error(
`${errors.length} errors were thrown when executing your fetchData functions.`,
);
error.queryErrors = errors;
throw error;
}
});
return Promise.all(mappedPromises);
}

function processErrors(errors: any[]) {
switch (errors.length) {
case 0:
break;
case 1:
throw errors.pop();
default:
const wrapperError: any = new Error(
`${errors.length} errors were thrown when executing your fetchData functions.`,
);
wrapperError.queryErrors = errors;
throw wrapperError;
}
}

export default function getDataFromTree(
rootElement: React.ReactNode,
rootContext: any = {},
): Promise<any> {
const errors: any[] = [];
const storeError = (error: any) => errors.push(error);

return getDataAndErrorsFromTree(rootElement, rootContext, storeError).then(_ =>
processErrors(errors),
);
}
83 changes: 82 additions & 1 deletion test/client/getDataFromTree.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,87 @@ describe('SSR', () => {
});
});

it('should return multiple errors in nested wrapped components without circular reference to wrapper error', () => {
const lastNameQuery = gql`
{
currentUser {
lastName
}
}
`;
interface LastNameData {
currentUser: {
lastName: string;
};
}
const firstNameQuery = gql`
{
currentUser {
firstName
}
}
`;
interface FirstNameData {
currentUser: {
firstName: string;
};
}

const userData = { currentUser: { lastName: 'Tester', firstName: 'James' } };
const link = mockSingleLink(
{
request: { query: lastNameQuery },
result: { data: userData },
delay: 50,
},
{
request: { query: firstNameQuery },
result: { data: userData },
delay: 50,
},
);
const apolloClient = new ApolloClient({
link,
cache: new Cache({ addTypename: false }),
});

interface Props {}

type WithLastNameProps = ChildProps<Props, LastNameData>;
const withLastName = graphql<Props, LastNameData>(lastNameQuery);

const BorkedComponent = () => {
throw new Error('foo');
};

const WrappedBorkedComponent = withLastName(BorkedComponent);

const ContainerComponent: React.StatelessComponent<WithLastNameProps> = ({ data }) => (
<div>
{!data || data.loading || !data.currentUser ? 'loading' : data.currentUser.lastName}
<WrappedBorkedComponent />
<WrappedBorkedComponent />
</div>
);

type WithFirstNameProps = ChildProps<Props, FirstNameData>;
const withFirstName = graphql<Props, FirstNameData>(firstNameQuery);

const WrappedContainerComponent = withFirstName(ContainerComponent);

const app = (
<ApolloProvider client={apolloClient}>
<WrappedContainerComponent />
</ApolloProvider>
);

return getDataFromTree(app).catch(e => {
expect(e.toString()).toEqual(expect.stringContaining('2 errors were thrown'));
expect(e.queryErrors.length).toBeGreaterThan(1);
expect(e.toString()).not.toEqual(e.queryErrors[0].toString());
});
});

it('should handle errors thrown by queries', () => {
const query = gql`
{
Expand Down Expand Up @@ -792,7 +873,7 @@ describe('SSR', () => {

return getDataFromTree(app).catch(e => {
expect(e).toBeTruthy();
expect(e.queryErrors.length).toEqual(1);
expect(e.queryErrors).toBeUndefined();

// But we can still render the app if we want to
const markup = ReactDOM.renderToString(app);
Expand Down