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

feat: add errorformatter #1273

Merged
merged 5 commits into from
Mar 16, 2023
Merged

feat: add errorformatter #1273

merged 5 commits into from
Mar 16, 2023

Conversation

juliusmarminge
Copy link
Member

Closes #

βœ… Checklist

  • I have followed every step in the contributing guide (updated 2022-10-06).
  • The PR title follows the convention we established conventional-commit
  • I performed a functional test on my final commit

Changelog

[Short description of what has changed]


Screenshots

[Screenshots]

πŸ’―

@changeset-bot
Copy link

changeset-bot bot commented Mar 11, 2023

πŸ¦‹ Changeset detected

Latest commit: 9fd84b3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
create-t3-app Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link

vercel bot commented Mar 11, 2023

The latest updates on your projects. Learn more about Vercel for Git β†—οΈŽ

Name Status Preview Comments Updated
create-t3-app βœ… Ready (Inspect) Visit Preview πŸ’¬ Add your feedback Mar 15, 2023 at 3:02AM (UTC)

@github-actions
Copy link
Contributor

Running Lighthouse audit...

Copy link
Member

@nexxeln nexxeln left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it, we should add docs for this too before merging

@juliusmarminge
Copy link
Member Author

juliusmarminge commented Mar 11, 2023

Love it, we should add docs for this too before merging

Good shout! What do you think would be the best docs for this?


Inferring errors

By default, tRPC sets up an error formatter that let's you infer your Zod Errors if you get validation errors on the backend.

Example usage:

function MyComponent() {
  const mutation = api.post.create.useMutation();

  return (
    <form>
      <input name="title" />
      {mutation.error?.data?.zodError?.fieldErrors.title ? (
        <span className="mb-8 text-red-500">
          {mutation.error.data.zodError.fieldErrors.title}
        </span>
      ) : null}

      ...
    </form>
  );
}

Or do you have a better idea?

@nexxeln
Copy link
Member

nexxeln commented Mar 11, 2023

Looks good to me

@c-ehrlich
Copy link
Member

Love it, we should add docs for this too before merging

Good shout! What do you think would be the best docs for this?

Inferring errors

By default, tRPC sets up an error formatter that let's you infer your Zod Errors if you get validation errors on the backend.

Example usage:

function MyComponent() {
  const mutation = api.post.create.useMutation();

  return (
    <form>
      <input name="title" />
      {mutation.error?.data?.zodError?.fieldErrors.title ? (
        <span className="mb-8 text-red-500">
          {mutation.error.data.zodError.fieldErrors.title}
        </span>
      ) : null}

      ...
    </form>
  );
}

Or do you have a better idea?

docs text should be "lets" instead of "let's"
everything else looks really good.

realized while reading this pr that maybe some of the text in trpc.ts could be made a bit easier to understand overall, but thats for another pr

Co-authored-by: Christopher Ehrlich <[email protected]>
@t3dotgg
Copy link
Member

t3dotgg commented Mar 15, 2023

This not being in yet just embarrassed the hell out of me during a recording πŸ˜…

@nexxeln
Copy link
Member

nexxeln commented Mar 15, 2023

I think we can merge this now

Copy link
Member

@c-ehrlich c-ehrlich left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shipppp

@c-ehrlich c-ehrlich added this pull request to the merge queue Mar 16, 2023
Merged via the queue into next with commit 10a5e0b Mar 16, 2023
@c-ehrlich c-ehrlich deleted the error-formatter branch March 16, 2023 13:17
@JClackett
Copy link

JClackett commented Mar 20, 2023

Has anyone run into the scenario where you are updating a resource and so need a data payload but also an id to actually find the resource update, the function would look something like this:

update: protectedProcedure.input(z.object({ id: z.string(), data: customSchemaForResource })).mutation(..

now when using the errorFormatter/flatten thing from zod, any errors within the data schema validator get flattened into a "data" key, and so on the frontend you dont know which field caused the error.

What are peoples practices for handling this?

@juliusmarminge
Copy link
Member Author

juliusmarminge commented Mar 20, 2023

Has anyone run into the scenario where you are updating a resource and so need a data payload but also an id to actually find the resource update, the function would look something like this:

update: protectedProcedure.input(z.object({ id: z.string(), data: customSchemaForResource })).mutation(..

now when using the errorFormatter/flatten thing from zod, any errors within the data schema validator get flattened into a "data" key, and so on the frontend you dont know which field caused the error.

What are peoples practices for handling this?

What do you mean? The current formatter would return this if you send a number instead of a string to the default query on a fresh app:

// query this:
const hello = api.example.hello.useQuery({ text: 1 });

// would result in this error:
hello.error?.shape?.data.zodError;
{
  "formErrors": [],
  "fieldErrors": {
    "text": [
      "Expected string, received number"
    ]
  }
}

so you have your fieldErrors with the fields that failed. I think this should be enough for the default case but obviously you can add your own custom logic to handle your own needs beyond this

@JClackett
Copy link

JClackett commented Mar 20, 2023

@juliusmarminge Just curious, how would you structure your trpc input when updating a "todo", for example? as you'll need an ID of some sort and then the actual data to update, do you just put the ID alongside all the fields that are being updated?

The reason I ask, is because if you have a nested object in a Zod schema, the flatten function removes all the nested keys and puts all the error message of the nested object into the top level key. for example:

z.object({
  id: z.string(),
  data: z.object({
    name: z.string(),
    description: z.string()
  })
})

If you flatten this, name and description get flattened into the "data" key:

{
  "formErrors": [],
  "fieldErrors": {
    "data": [ // if the name was a number, it still shows up on the data key
      "Expected string, received number"
    ]
  }
}

@juliusmarminge
Copy link
Member Author

Yea probably, i had some procedure recently which posted a review on a product which was something like

create: protectedProcedure
  .input(z.object({
    productId: z.string(),
    text: z.string().min(10),
    rating: z.number().min(0).max(5),
  })
  .mutation(...)

@JClackett
Copy link

JClackett commented Mar 20, 2023

Yea probably, i had some procedure recently which posted a review on a product which was something like

create: protectedProcedure
  .input(z.object({
    productId: z.string(),
    text: z.string().min(10),
    rating: z.number().min(0).max(5),
  })
  .mutation(...)

Yeah exactly so just need to make sure to extract it from the input before updating a database record for example, as Ive typically been doing:

await prisma.todo.update({where: { id: input.id }, data: input.data })

Was just wondering how people where structuring their inputs, thanks for the help!

P.s. the reason I was trying to avoid merging the ID next to the data, is because if the data object is quite large, and reused in many places, I abstracted into a const somewhere, but for updating I now need to merge the ID into this?

p.p.s was easy enough to merge actually

taskSchema.merge(z.object({id: z.string()}))

@juliusmarminge
Copy link
Member Author

Ah I see...

Something like this:

export const exampleRouter = createTRPCRouter({
  hello: publicProcedure
    .input(
      z.object({
        id: z.string(),
        data: z.object({
          text: z.string().min(3),
        }),
      })
    )
    .query(({ input }) => {
      return {
        greeting: `Hello ${input.data.text}`,
      };
    }),
});

would return an error like this if some field within the data object is invalid:

{
  "formErrors": [],
  "fieldErrors": {
    "data": [
      "String must contain at least 3 character(s)"
    ]
  }
}

Hmm... not sure if you can write a better error formatter to handle that form of input structure

@JClackett
Copy link

Yeah exactly, I edited my previous comment showing a workaround ill go for as I really dont wana write my own formatter :P

@juliusmarminge
Copy link
Member Author

The .format() (instead of .flatten()) method on zodErrors give you something like this:

{
  "_errors": [],
  "id": {
    "_errors": [
      "Expected string, received number"
    ]
  },
  "data": {
    "_errors": [],
    "text": {
      "_errors": [
        "String must contain at least 3 character(s)"
      ]
    }
  }
}

which you could probably work with to make it work for your usecase

@JClackett
Copy link

JClackett commented Mar 20, 2023

The .format() (instead of .flatten()) method on zodErrors give you something like this:

{
  "_errors": [],
  "id": {
    "_errors": [
      "Expected string, received number"
    ]
  },
  "data": {
    "_errors": [],
    "text": {
      "_errors": [
        "String must contain at least 3 character(s)"
      ]
    }
  }
}

which you could probably work with to make it work for your usecase

Yeah looked into this but was getting some issues with TS typings, but will investigate more, thanks for help!

Is there a way to get type safe access to the field errors?

as updateTask.error?.data?.zodError?.fieldErrors?.name, the property name is unknown on the frontend

p.s would be real nice if t3-stack could show examples of typesafe error handling

@juliusmarminge
Copy link
Member Author

juliusmarminge commented Mar 20, 2023

Yeah looked into this but was getting some issues with TS typings, but will investigate more, thanks for help!

Aah that's a shame. Doesn't seem like that's typed :/

as updateTask.error?.data?.zodError?.fieldErrors?.name, the property name is unknown on the frontend

It shouldn't be:

CleanShot 2023-03-20 at 18 19 30@2x

@JClackett
Copy link

Hmm actually the field is typed once you've actually written it, just the autocomplete doesn't work for the fieldErrors. Do you get autocomplete when typing ".id" then?

My field errors property is typed as so:

Screenshot 2023-03-20 at 19 47 32

devvianto605 pushed a commit to devvianto605/create-devviantex-nextjs-app-deprecated that referenced this pull request Jun 9, 2024
* add errorformatter

* add docs

* changeset

* Update www/src/pages/en/usage/trpc.md

Co-authored-by: Christopher Ehrlich <[email protected]>

---------

Co-authored-by: Christopher Ehrlich <[email protected]>
Co-authored-by: Shoubhit Dash <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
πŸ“Œ area: cli Relates to the CLI πŸ“Œ area: t3-app Relates to the generated T3 App πŸ“š documentation Improvements or additions to documentation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants