Learn the origin of this error and learn how to properly handle it.
In the previous chapter, you encountered an error while executing a Mutation.postCommentOnLink
operation. In this chapter, you will learn the origin of this error and learn how to properly handle
it.
Recap of the Encountered Error
This is the resolver implementation that caused the issue when using a linkId that has no
counter-part within the database.
As you can see the error includes an extensionsoriginalError field. It includes a full
stacktrace of the original error for finding the cause easily.
Yoga Error Masking
In a production environment, a stacktrace that leaks to the outside world are a potential security
threat as that information could be misused by malicious actors.
The extensionsoriginalError field is only present within an error if the server has been
started with the NODE_ENV environment variable being set to development.
As you might remember this is only the case when you are starting the server using npm run dev.
Start the GraphQL server using the npm run start script.
npm run start
Then execute the same operation again.
mutation postCommentOnLink { postCommentOnLink(linkId: "99999999999", body: "This is my second comment!") { id body }}
Now you can see that there is no originalError within the errors array of the first object.
This is great, but now we never know why our request failed 🤔 and the error message
Unexpected error. is also not helpful to anyone not aware of the exact resolver function
implementation.
Exposing Safe Error Messages
Let’s change the implementation to expose the much more helpful message
Cannot post comment on non-existing link with id 'X'. instead of Unexpected error..
For doing so you will use the GraphQLError class that is exported from the graphql package.
Add the following catch handler for mapping a foreign key error into a GraphQLError:
src/schema.ts
// ... other imports ...import { GraphQLError } from 'graphql'import { Prisma } from '@prisma/client'// ... other imports ...const resolvers = { // ... other resolver maps ... Mutation: { // ... other Mutation object type field resolver functions ... async postCommentOnLink( parent: unknown, args: { linkId: string; body: string }, context: GraphQLContext ) { const newComment = await context.prisma.comment .create({ data: { body: args.body, linkId: parseInt(args.linkId) } }) .catch((err: unknown) => { if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2003') { return Promise.reject( new GraphQLError(`Cannot post comment on non-existing link with id '${args.linkId}'.`) ) } return Promise.reject(err) }) return newComment } }}
The error code P2003 indicates a foreign key constraint error. You can learn more about it in
the Prisma documentation.
You use that code for identifying this edge case of a missing link and then throw a GraphQLError
with a useful error message instead.
Restart the server using npm run dev and again execute the mutation operation using GraphiQL.
mutation postCommentOnLink { postCommentOnLink(linkId: "99999999999", body: "This is my second comment!") { id body }}
You will receive a response with the
Cannot post comment on non-existing link with id '99999999999'. message:
{ "errors": [ { "message": "Cannot post comment on non-existing link with id '99999999999'.", "locations": [ { "line": 2, "column": 3 } ], "path": ["postCommentOnLink"] } ], "data": null}
As you might have noticed, GraphQL Yoga will exclude wrapped errors thrown within the resolvers from
masking the error masking.
That means every time you want to expose an error to the outside world you should throw such an
error.
At the same time, any other unexpected error will automatically be masked from the outside world,
bringing you sensible and safe defaults for a GraphQL Yoga production deployment!
Field Argument Sanitizing
There is one more issue within The Mutation.postCommentOnLink field resolver.
So far you only executed the mutation operation while using a string encoded integer value for the
linkId argument.
Execute that operation again using this non-integer value:
mutation postCommentOnLink { postCommentOnLink(linkId: "11a", body: "This is my second comment!") { id body }}
All seems good, right?
{ "errors": [ { "message": "Cannot post comment on non-existing link with id '11a'.", "locations": [ { "line": 2, "column": 3 } ], "path": ["postCommentOnLink"] } ], "data": null}
Let’s also try another non-integer string value.
Execute that operation again using this non-integer value:
mutation { postCommentOnLink(linkId: "uuuuuu", body: "This is my second comment!") { id body }}
What the heck is happening now?
{ "errors": [ { "message": "Unexpected error.", "locations": [ { "line": 2, "column": 3 } ], "path": ["postCommentOnLink"], "extensions": { "originalError": { "message": "\nInvalid `.create()` invocation in\nhackernews/src/schema.ts:93:10\n\n 90 context: GraphQLContext,\n 91 ) => {\n 92 const comment = await context.prisma.comment\n→ 93 .create({\n data: {\n body: 'This is my second comment!',\n linkId: NaN\n ~~~~~~\n }\n })\n\nUnknown arg `linkId` in data.linkId for type CommentCreateInput. Did you mean `link`? Available args:\ntype CommentCreateInput {\n createdAt?: DateTime\n body: String\n link?: LinkCreateNestedOneWithoutCommentsInput\n}\n\n", "stack": "Error: \nInvalid `.create()` invocation in\nhackernews/src/schema.ts:93:10\n\n 90 context: GraphQLContext,\n 91 ) => {\n 92 const comment = await context.prisma.comment\n→ 93 .create({\n data: {\n body: 'This is my second comment!',\n linkId: NaN\n ~~~~~~\n }\n })\n\nUnknown arg `linkId` in data.linkId for type CommentCreateInput. Did you mean `link`? Available args:\ntype CommentCreateInput {\n createdAt?: DateTime\n body: String\n link?: LinkCreateNestedOneWithoutCommentsInput\n}\n\n\n at Object.validate (hackernews/node_modules/@prisma/client/runtime/index.js:34786:20)\n at PrismaClient._executeRequest (hackernews/node_modules/@prisma/client/runtime/index.js:40911:17)\n at consumer (hackernews/node_modules/@prisma/client/runtime/index.js:40856:23)\n at hackernews/node_modules/@prisma/client/runtime/index.js:40860:76\n at runInChildSpan (hackernews/node_modules/@prisma/client/runtime/index.js:39945:12)\n at hackernews/node_modules/@prisma/client/runtime/index.js:40860:20\n at AsyncResource.runInAsyncScope (async_hooks.js:197:9)\n at PrismaClient._request (hackernews/node_modules/@prisma/client/runtime/index.js:40859:86)\n at hackernews/node_modules/@prisma/client/runtime/index.js:40190:25\n at _callback (hackernews/node_modules/@prisma/client/runtime/index.js:39960:52)" } } } ], "data": null}
You just encountered another unexpected error that got masked.
Let’s analyze the error message:
Invalid `.create()` invocation in hackernews/src/schema.ts:93:10Unknown arg `linkId` in data.linkId for type CommentCreateInput.linkId: NaN
These are the three parts within the error message that should make it click.
It seems like the linkId passed to the prisma create function is NaN, which is a special value
for “not a number” in JavaScript. The underlying database, however, expects an integer value. The
error is raised and thrown, as you did not add logic for handling this edge case, yet.
But why did the previous value "11a", not result in such an error?
To clarify that question we need to have a small excursion on how the
parseInt
function works.
There are multiple mathematical numeral systems. The implementation tries to be a bit smart and
fault-tolerant. So, let’s build a quick custom function, parseIntSafe, that first validates the
string contents using a regex.
Add the following code to the src/schema.ts file.
src/schema.ts
// ... other code ...import { GraphQLError } from 'graphql'import { Prisma } from '@prisma/client'const parseIntSafe = (value: string): number | null => { if (/^(\d+)$/.test(value)) { return parseInt(value, 10) } return null}const resolvers = { // ... other resolver maps ... Mutation: { // ... other field resolver functions async postCommentOnLink( parent: unknown, args: { linkId: string; body: string }, context: GraphQLContext ) { const linkId = parseIntSafe(args.linkId) if (linkId === null) { return Promise.reject( new GraphQLError(`Cannot post comment on non-existing link with id '${args.linkId}'.`) ) } const newComment = await context.prisma.comment .create({ data: { linkId, body: args.body } }) .catch((err: unknown) => { if (err instanceof Prisma.PrismaClientKnownRequestError) { if (err.code === 'P2003') { return Promise.reject( new GraphQLError( `Cannot post comment on non-existing link with id '${args.linkId}'.` ) ) } } return Promise.reject(err) }) return newComment } }}
The regex /^(\d+)$/ simply verifies that every character within the value is a digit. In case
the value includes a non-digit character, you simply return null and then reject the resolver with
a GraphQLYogaError error.
Returning null instead of returning NaN is the better choice here, as it allows nice TypeScript
checks (linkId === null).
Execute that operation again using this non-integer value:
mutation postCommentOnLink { postCommentOnLink(linkId: "uuuuuu", body: "This is my second comment!") { id body }}
All is good now. 🎉
{ "errors": [ { "message": "Cannot post comment on non-existing link with id 'uuuuuu'.", "locations": [ { "line": 2, "column": 3 } ], "path": ["postCommentOnLink"] } ], "data": null}
Remember, when implementing your GraphQL resolver functions, the input arguments should always be
sanitized and validated!
Optional Exercise
As an optional exercise for interiorizing the knowledge, you can now also implement validation of
the body argument of Mutation.postCommentOnLink and argument sanitization for the
Mutation.postLink field. You wouldn’t want anyone to post an empty comment or invalid link, right?
This site uses cookies for analytics and improving your experience.