Error handling
aws-appsync-js throws typed error classes. Catch the base class for a single catch block, or instanceof the specific subclass when you need to discriminate. Each error also has a stable code field you can branch on across module boundaries.
The taxonomyβ
AppSyncNetworkErrorβ
The HTTP request itself failed β DNS lookup, TCP connect, TLS handshake, CORS preflight, or fetch rejected. The underlying error is on .cause.
catch (err) {
if (err instanceof AppSyncNetworkError) {
console.log(err.message, err.cause);
}
}
Retried by default (network errors are usually transient).
AppSyncAbortErrorβ
The request was cancelled via AbortSignal or hit the configured timeout. reason distinguishes the two:
catch (err) {
if (err instanceof AppSyncAbortError) {
return err.reason === 'timeout' ? 'too slow' : 'user cancelled';
}
}
Not retried by default β abort is a deliberate signal.
AppSyncHttpErrorβ
AppSync returned a non-2xx status. The status code and the parsed body (if it was JSON) are on the error so you can build a meaningful UI:
catch (err) {
if (err instanceof AppSyncHttpError) {
console.log(err.status, err.statusText, err.body);
if (err.status === 401) return refreshTokenAndRetry();
if (err.status === 403) return showAccessDeniedScreen();
}
}
Retried by default on 5xx and 429.
AppSyncGraphQLErrorβ
HTTP was 200 but the server returned a non-empty errors array. Per the GraphQL spec, partial data is allowed β you'll find it on .data:
catch (err) {
if (err instanceof AppSyncGraphQLError) {
// err.errors is GraphQLFormattedError[]
for (const e of err.errors) {
console.error(e.message, e.path, e.extensions);
}
// err.data may be partially populated
renderPartial(err.data);
}
}
Not retried by default β GraphQL errors are usually deterministic (invalid input, unauthorized field, β¦).
Picking the right catchβ
import {
AppSyncError, // base class
AppSyncNetworkError,
AppSyncAbortError,
AppSyncHttpError,
AppSyncGraphQLError,
} from 'aws-appsync-js';
try {
return await client.request(query);
} catch (err) {
if (err instanceof AppSyncGraphQLError) return handleGraphQL(err);
if (err instanceof AppSyncHttpError) return handleHttp(err);
if (err instanceof AppSyncAbortError) return handleAbort(err);
if (err instanceof AppSyncNetworkError) return handleNetwork(err);
if (err instanceof AppSyncError) return handleUnknown(err);
throw err; // not from this library β rethrow
}
Switching on code instead of instanceofβ
When you're crossing module boundaries (different packages, micro-frontends, serverβbrowser), instanceof can fail because the class identities are duplicated. Switching on code works regardless:
switch ((err as { code?: string }).code) {
case 'GRAPHQL_ERROR': /* β¦ */ break;
case 'HTTP_ERROR': /* β¦ */ break;
case 'ABORTED': /* β¦ */ break;
case 'NETWORK_ERROR': /* β¦ */ break;
}
Common GraphQL errorType values from AppSyncβ
AppSync embeds an errorType in extensions you can inspect:
errorType | Meaning |
|---|---|
Unauthorized | @auth rule denied the field |
MissingAuthenticationToken | No auth header sent (or wrong header for the configured mode) |
ExecutionTimeout | Resolver took longer than the configured timeout |
Throttling | Per-API or per-resolver throttle limit hit |
DynamoDB:ConditionalCheckFailedException | The classic conditional-update conflict |
for (const e of err.errors) {
if (e.extensions?.['errorType'] === 'Throttling') {
await sleep(1000);
return retry();
}
}
What you don't have to writeβ
- No try/catch boilerplate to parse the AppSync error envelope β that's done for you.
- No string-matching on error messages β every error has
codeand (where relevant)status,reason, orerrors. - No second
try/catchfor JSON parse failures β those surface asAppSyncNetworkErrorwith the JSON-parse error on.cause.