Error handling
Go: explicit + wrapped. React: error boundary + toast.
Errors are normal; how you handle them is what makes the code production-ready. Grit has one pattern for Go (explicit + wrapped) and one for React (error boundary + toast). Both close the loop so no error is silently swallowed.
Go side — explicit, wrapped, never ignored
func (s *UserService) Create(ctx context.Context, in CreateUserInput) (*User, error) {hashed, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost)if err != nil {return nil, fmt.Errorf("hashing password: %w", err)}user := &User{Email: in.Email, PasswordHash: hashed}if err := s.db.Create(user).Error; err != nil {return nil, fmt.Errorf("creating user: %w", err)}return user, nil}
The three rules:
- Every error is checked. Never
_ := someFunc(). - Errors are wrapped with context.
fmt.Errorf("doing X: %w", err). The%wmakes it unwrappable upstream (witherrors.Is/errors.As). - Handlers convert errors to HTTP responses. Services return errors; handlers map them to status codes via a tiny helper.
The handler-side helper
func (h *UserHandler) Create(c *gin.Context) {var in services.CreateUserInputif err := c.ShouldBindJSON(&in); err != nil {respond.Error(c, 422, "VALIDATION_ERROR", err)return}user, err := h.svc.Create(c.Request.Context(), in)if err != nil {respond.Error(c, 500, "INTERNAL_ERROR", err)return}respond.Created(c, user, "User created")}
respond is a tiny package that shapes the envelope you learned in the last lesson.
React side — error boundary + toast
export function useCreateUser() {return useMutation({mutationFn: api.users.create,onSuccess: () => toast.success("User created"),onError: (err: ApiError) => {if (err.code === "VALIDATION_ERROR") {// form-level handling — see details.email etc.return}toast.error(err.message || "Something went wrong")},})}
Above the page, an <ErrorBoundary> catches anything the mutations / queries don't — including render-time crashes. Users see a friendly fallback, not a white screen.
Quick check
Try it
Trigger every error path in a real request. With the API running, use curl or the OpenAPI docs to hit:
POST /api/auth/loginwith no body — capture the 422 response innotes.md.POST /api/auth/loginwith valid email + wrong password — capture the 401.GET /api/users/00000000-0000-0000-0000-000000000000— capture the 404.
You should see Shape 3 in all three cases.
What's next
Chapter 4 — Code generation. The most fun command in Grit. grit generate resource Product writes 7 files and wires them all together. We tour them line by line.
Spot a typo? Have an idea?
Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled — suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.
Suggest an improvement on GitHub