thanos-io/thanos · error

BUG: errors.Cause returned nil on a non-nil error

Error message

BUG: errors.Cause returned nil on a non-nil error

What it means

This is a deliberate invariant panic: after wrapping, the handler unwraps the first error and calls errors.Cause(err) (github.com/pkg/errors); if Cause returns nil for a non-nil error the code panics, because the subsequent cause-switch cannot classify the failure. It guards against accidentally wrapping a non-pkg/errors error (e.g. a plain fmt.Errorf without %w or a stdlib error with no Cause support).

Solutions

  1. File/inspect a Thanos bug — this panic indicates a violated internal wrapping contract, not a user error
  2. Find which writer.Write error chain produced nil Cause and fix it to wrap with pkg/errors (errors.Wrapf) or fmt.Errorf with %w
  3. As a workaround, ensure h.writer returns errors created/wrapped via pkg/errors so errors.Cause is defined
  4. Upgrade Thanos to a version where the receive error classification handles non-pkg/errors chains

Example fix

// before: writer returns a plain error with no cause
return errors.New("tsdb closed")
// after: keep a cause chain pkg/errors can resolve
return errors.Wrap(tsdb.ErrClosed, "writing samples")
Defensive patterns

Strategy: type-guard

Validate before calling

// guard before classifying: ensure the chain yields a non-nil cause
if err != nil {
    if c := errors.Cause(err); c == nil {
        return errors.Wrap(err, "unclassifiable write failure")
    }
}

Type guard

func hasCause(err error) bool {
    return err != nil && errors.Cause(err) != nil
}

Try / catch

defer func() {
    if r := recover(); r != nil {
        logger.Error("receive handler panicked classifying write error", "panic", r)
        // respond Unavailable so clients retry rather than lose data
    }
}()

Prevention

When it happens

Trigger: The first collected write error, after errors.Unwrap, has no pkg/errors cause: errors.Cause returns the error itself normally, but nil is returned when the chain is broken — e.g. errs[0] is nil, or the wrapped cause at handler.go:1432 does not support Cause and Unwrap produced nil.

Common situations: A writer implementation returning a bare error that escapes the wrapping contract; a code change replacing pkg/errors wrapping with plain errors.New somewhere in the Write path; genuine Thanos bug.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of thanos-io/thanos@35b8b99117 (2026-09-07). Data as JSON: /api/errors/15d3d5673bb0536a. Report an issue: GitHub.

Appendix: source

Thrown at pkg/receive/handler.go:1432

			err := h.writer.Write(ctx, di.tenant, di.wreq.Timeseries)
			if err != nil {
				level.Debug(h.logger).Log("msg", "failed to write to local TSDB", "err", err, "tenant", di.tenant)

				errs = append(errs, fmt.Errorf("writing %s data to local TSDB: %w", di.tenant, err))
			}
		}

		if len(errs) > 0 {
			returnErr := errs[0]
			err := errors.Unwrap(returnErr)

			if len(errs) > 1 {
				returnErr = fmt.Errorf("got %d errors while writing to multiple tenants, first one: %w", len(errs), returnErr)
			}

			switch cause := errors.Cause(err); cause {
			case nil:
				panic("BUG: errors.Cause returned nil on a non-nil error")
			default:
				if isNotReady(cause) {
					return nil, status.Error(codes.Unavailable, returnErr.Error())
				}
				if isConflict(cause) {
					return nil, status.Error(codes.AlreadyExists, returnErr.Error())
				}
				return nil, status.Error(codes.Internal, returnErr.Error())
			}
		}

		return &storepb.WriteResponse{}, nil

	}

	_, err := h.handleRequest(ctx, uint64(r.Replica), data)
	if err != nil {
		level.Debug(h.logger).Log("msg", "failed to handle request", "err", err)

View on GitHub (pinned to 35b8b99117)