# Exception handling

> Return expected failures as tinka.Error, map Capability errors with ErrorMap, and customize how errors render.

This guide covers how Tinka turns errors into responses. You will learn how to:

- Return expected failures with the right status and message
- Map Capability errors to responses in one place
- Read the problem details clients receive
- Render errors your own way
- Know what the App handles on its own

## Overview

A Tinka handler returns an error instead of writing failure responses itself. The App renders it: an expected failure becomes RFC 9457 problem details with its status and message, and anything else becomes a generic `500` that's logged with the request's ID. Internal details, such as a database address in an error message, never reach the client.

## Return an expected failure

Return one of the constructors when the client should know what went wrong. Each takes a message for people, formatted like `fmt.Sprintf`.

```go title="posts.go"
package main

import (
	"net/http"

	"github.com/mucolabs/tinka"
)

func showDraft(w http.ResponseWriter, r *http.Request) error {
	id, err := tinka.PathInt(r, "id")
	if err != nil {
		return err
	}
	if id > 100 {
		return tinka.NotFound("No draft %d.", id)
	}
	return tinka.JSON(w, http.StatusOK, map[string]int64{"id": id})
}
```

The constructors cover the common statuses:

- **`BadRequest`**: `400`, the request is malformed
- **`Unauthorized`**: `401`, the request needs valid credentials
- **`Forbidden`**: `403`, the credentials are valid but not allowed this
- **`NotFound`**: `404`, the thing asked for doesn't exist
- **`Conflict`**: `409`, the request clashes with the current state
- **`Invalid`**: `422`, one input field is invalid

For anything else, return a `*tinka.Error` with your own status and a stable `Code` that clients can match on:

```go
	return &tinka.Error{
		Status:  http.StatusPaymentRequired,
		Code:    "card_declined",
		Message: "The card was declined.",
		Hint:    "Try another card or contact your bank.",
	}
```

## Map Capability errors to responses

A Capability returns sentinel errors such as `ErrEmailTaken`, with no HTTP in sight. A `tinka.ErrorMap` turns them into responses in one place, and any error it doesn't know passes through as a logged `500`. Here the Capability's errors sit beside the handler to keep the example in one file:

```go title="signup.go"
package main

import (
	"context"
	"errors"
	"net/http"

	"github.com/mucolabs/tinka"
)

var (
	ErrInvalidEmail = errors.New("invalid email address")
	ErrEmailTaken   = errors.New("email address already has an account")
)

var signupErrors = tinka.ErrorMap{
	ErrInvalidEmail: tinka.Invalid("email", "The email address is invalid."),
	ErrEmailTaken:   tinka.Conflict("An account already uses this email."),
}
```

The handler calls the Action and maps whatever it refuses with:

```go title="signup.go" ins={1-18}
type signup struct {
	register func(ctx context.Context, email string) (int64, error)
}

type signupForm struct {
	Email string `json:"email"`
}

func (h signup) execute(w http.ResponseWriter, r *http.Request) error {
	in, err := tinka.Decode[signupForm](w, r)
	if err != nil {
		return err
	}
	id, err := h.register(r.Context(), in.Email)
	if err != nil {
		return signupErrors.Map(err)
	}
	return tinka.JSON(w, http.StatusCreated, map[string]int64{"id": id})
}
```

`Map` matches wrapped errors with `errors.Is`, and the response it returns keeps the original error as its cause, so the log still shows what happened.

## Read the response clients get

Clients receive problem details with `Content-Type: application/problem+json`. `code`, `hint`, `fields` and `request_id` extend the standard members:

```json title="Response"
{
  "title": "Conflict",
  "status": 409,
  "detail": "An account already uses this email.",
  "code": "conflict",
  "request_id": "S5QWQ3DAEXTVZ6M7F4DZEL6EBI"
}
```

A browser that asks for HTML gets the same information as a small HTML page instead. The `request_id` matches the `X-Request-Id` response header and the log, so a user's report leads straight to the log record.

## Render errors your own way

`OnError` replaces the default rendering, for example with your own HTML pages. It receives the error as the client should see it. If it returns an error, the App sends the default problem details instead.

```go title="main.go"
	app.OnError(func(
		w http.ResponseWriter, r *http.Request, e *tinka.Error,
	) error {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		w.WriteHeader(e.Status)
		_, err := fmt.Fprintf(w, "%d: %s\n", e.Status, e.Message)
		return err
	})
```

## Know what the App does on its own

A few failures need no code from you:

- **A panic** in a handler becomes a `500`, and the App keeps serving
- **A client that disconnects** cancels the request's context; a handler that returns that error writes nothing back
- **An error after the response started** is logged, because a second response can't be sent
- **A context deadline** from `tinka.Timeout` becomes a `503`

See also: [App](../../reference/app.md), [Body parser](body-parser.md)
