Skip to content

Commit 79ad818

Browse files
authored
Improve Apollo error handling and log missing repositories gracefully
Previously, uncaught ApolloErrors — especially those triggered by exceeding the GitHub API rate limit — surfaced as unhandled promise rejections. This caused confusing, bloated error stacks in the console and toast messages, detracting from the user experience and masking the root issue. Let’s unwrap promise rejections at the top level and explicitly detect Apollo errors. By inspecting graphQLErrors and networkError, we can now display clear, user-friendly messages when rate limits are hit or data is unavailable. Additionally, GraphQL responses sometimes lacked repository fields, often due to upstream errors or permission issues. The previous implementation assumed response.body.data.repository would exist, causing runtime crashes or broken logic when it didn’t. Let’s add guards and structured logging to flag when repositories are missing, including relevant operationName context to help with debugging.
1 parent 10b44b1 commit 79ad818

File tree

2 files changed

+70
-8
lines changed

2 files changed

+70
-8
lines changed

src/app/app.module.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,39 @@ import { SharedModule } from './shared/shared.module';
8686
bootstrap: [AppComponent]
8787
})
8888
export class AppModule {
89-
constructor(private apollo: Apollo, private httpLink: HttpLink, private authService: AuthService, private logger: LoggingService) {
89+
constructor(
90+
private apollo: Apollo,
91+
private httpLink: HttpLink,
92+
private authService: AuthService,
93+
private logger: LoggingService,
94+
private errorHandler: ErrorHandlingService
95+
) {
9096
const URI = 'https://api.github.com/graphql';
9197
const log = new ApolloLink((operation, forward) => {
92-
operation.setContext({ start: performance.now() });
98+
const start = performance.now();
99+
operation.setContext({ start });
100+
93101
this.logger.info('AppModule: GraphQL request', operation.getContext());
102+
94103
return forward(operation).map((result) => {
95-
const time = performance.now() - operation.getContext().start;
96-
this.logger.info('AppModule: GraphQL response', operation.getContext(), `in ${Math.round(time)}ms`);
97-
const repo = operation.getContext().response.body.data.repository;
104+
const time = Math.round(performance.now() - start);
105+
const context = operation.getContext();
106+
107+
this.logger.info('AppModule: GraphQL response', context, `in ${time}ms`);
108+
109+
const repo = context.response?.body?.data?.repository;
110+
if (!repo) {
111+
this.logger.warn('AppModule: GraphQL response missing repository', {
112+
context: context.name,
113+
operation: operation.operationName
114+
});
115+
return result;
116+
}
117+
98118
const item = Object.keys(repo)[0];
99-
this.logger.debug('AppModule: GraphQL response body', item, repo[item].edges.length, repo[item].edges);
119+
const edges = repo[item]?.edges ?? [];
120+
this.logger.debug('AppModule: GraphQL response body', item, edges.length, edges);
121+
100122
return result;
101123
});
102124
});

src/app/core/services/error-handling.service.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { HttpErrorResponse } from '@angular/common/http';
22
import { ErrorHandler, Injectable } from '@angular/core';
33
import { MatSnackBar } from '@angular/material/snack-bar';
4+
import { ApolloError } from '@apollo/client/core';
45
import { RequestError } from '@octokit/request-error';
6+
import { GraphQLError } from 'graphql';
57
import { FormErrorComponent } from '../../shared/error-toasters/form-error/form-error.component';
68
import { GeneralMessageErrorComponent } from '../../shared/error-toasters/general-message-error/general-message-error.component';
79
import { LoggingService } from './logging.service';
@@ -10,14 +12,52 @@ export const ERRORCODE_NOT_FOUND = 404;
1012

1113
const FILTERABLE = ['node_modules'];
1214

15+
interface RejectionErrorWrapper {
16+
rejection?: unknown;
17+
}
18+
19+
function hasRejection(error: unknown): error is RejectionErrorWrapper {
20+
return typeof error === 'object' && error !== null && 'rejection' in error;
21+
}
22+
1323
@Injectable({
1424
providedIn: 'root'
1525
})
1626
export class ErrorHandlingService implements ErrorHandler {
1727
constructor(private snackBar: MatSnackBar, private logger: LoggingService) {}
1828

19-
handleError(error: HttpErrorResponse | Error | RequestError, actionCallback?: () => void) {
29+
handleError(
30+
error: HttpErrorResponse | Error | RequestError | ApolloError | RejectionErrorWrapper,
31+
actionCallback?: () => void
32+
): void {
33+
if (hasRejection(error)) {
34+
error = error.rejection;
35+
}
36+
37+
if (error instanceof ApolloError) {
38+
if (error.graphQLErrors?.length) {
39+
error.graphQLErrors.forEach((gqlError: GraphQLError) => {
40+
const isRateLimit = gqlError.message.toLowerCase().includes('rate limit');
41+
const message = isRateLimit
42+
? 'GitHub API rate limit exceeded. Please try again later.'
43+
: gqlError.message;
44+
45+
this.logger.error(`[GraphQL error]: ${gqlError.message}`);
46+
47+
this.snackBar.openFromComponent(GeneralMessageErrorComponent, {
48+
data: { message },
49+
});
50+
});
51+
} else if (error.networkError) {
52+
this.handleHttpError(error.networkError as HttpErrorResponse, actionCallback);
53+
} else {
54+
this.handleGeneralError(error.message);
55+
}
56+
return;
57+
}
58+
2059
this.logger.error(error);
60+
2161
if (error instanceof Error) {
2262
this.logger.debug('ErrorHandlingService: ', this.cleanStack(error.stack));
2363
}
@@ -26,7 +66,7 @@ export class ErrorHandlingService implements ErrorHandler {
2666
} else if (error.constructor.name === 'RequestError') {
2767
this.handleHttpError(error as RequestError, actionCallback);
2868
} else {
29-
this.handleGeneralError(error.message || JSON.stringify(error));
69+
this.handleGeneralError((error as Error).message || JSON.stringify(error));
3070
}
3171
}
3272

0 commit comments

Comments
 (0)