> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gocopper.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Handling

Copper provides the `cerrors` package, which adds two things to Go's plain errors: a message chain and **structured tags**. If you wrap errors on the way up, by the time one reaches your logs it will tell the whole story.

### Wrapping Errors

You may wrap an error with `cerrors.New(cause, message, tags)`:

```go theme={null}
launch, err := s.queries.GetLaunch(ctx, id)
if err != nil {
	return nil, cerrors.New(err, "failed to get launch", map[string]any{
		"launchId": id,
	})
}
```

Tags carry the context that a message alone cannot, such as IDs, states, and attempt counts. You should pass values directly rather than formatting them into the message.

If you would like to create a new error rather than wrap one, pass a `nil` cause:

```go theme={null}
if rocket.Fuel < required {
	return cerrors.New(nil, "insufficient fuel for launch", map[string]any{
		"have": rocket.Fuel,
		"need": required,
	})
}
```

When rendered, a wrapped error reads as a chain:

```
failed to launch rocket where rocketId=falcon-9 because
> failed to get launch where launchId=lc-42 because
> sql: no rows in result set
```

### Sentinel Errors

You should keep sentinel errors as plain `errors.New` values so that callers can match them with `errors.Is`:

```go theme={null}
var (
	ErrLaunchNotFound  = errors.New("launch not found")
	ErrAlreadyLaunched = errors.New("rocket has already launched")
)
```

You may return them directly or wrapped. Since `cerrors.Error` implements `Unwrap`, `errors.Is` and `errors.As` see through the chain either way.

### Handling Errors in Routers

At the HTTP boundary, you may switch on sentinels to map errors to status codes, treating everything else as a 500:

```go theme={null}
launch, err := ro.rockets.Launch(ctx, id)
switch {
case errors.Is(err, ErrLaunchNotFound):
	ro.rw.WriteJSON(w, chttp.WriteJSONParams{
		StatusCode: http.StatusNotFound,
		Data:       map[string]string{"error": "launch not found"},
	})
	return
case err != nil:
	ro.logger.Error("Failed to launch rocket", err)
	w.WriteHeader(http.StatusInternalServerError)
	return
}
```

Expected failures receive a helpful response body, while unexpected ones are logged with the full chain and return a bare 500. For the complete handler pattern, including request validation, see [JSON APIs](/http/json).

### Errors in Logs

You never need to log tags separately. When an error is passed to the logger, tags from the entire error chain are merged into the log's structured output automatically. See [Logging](/the-basics/logging) for more information.

<Note>
  `cerrors.Error` is a value type, not a pointer. If you ever need `errors.As`, match against `cerrors.Error`, not `*cerrors.Error`.
</Note>
