grpc/grpc-go · error

last connection error: %v; last resolver error: %v

Error message

last connection error: %v; last resolver error: %v

What it means

Produced by baseBalancer.mergeErrors when TransientFailure has BOTH a connection error and a resolver error. This combined form is the most diagnostic: it tells you both layers are failing (e.g. the resolver returned a partial/bad update AND the existing SubConns can't connect). Both underlying errors are concatenated so you can triage in one message.

Source

Thrown at balancer/base/balancer.go:158

	}

	b.regeneratePicker()
	b.cc.UpdateState(balancer.State{ConnectivityState: b.state, Picker: b.picker})
	return nil
}

// mergeErrors builds an error from the last connection error and the last
// resolver error.  Must only be called if b.state is TransientFailure.
func (b *baseBalancer) mergeErrors() error {
	// connErr must always be non-nil unless there are no SubConns, in which
	// case resolverErr must be non-nil.
	if b.connErr == nil {
		return fmt.Errorf("last resolver error: %v", b.resolverErr)
	}
	if b.resolverErr == nil {
		return fmt.Errorf("last connection error: %v", b.connErr)
	}
	return fmt.Errorf("last connection error: %v; last resolver error: %v", b.connErr, b.resolverErr)
}

// regeneratePicker takes a snapshot of the balancer, and generates a picker
// from it. The picker is
//   - errPicker if the balancer is in TransientFailure,
//   - built by the pickerBuilder with all READY SubConns otherwise.
func (b *baseBalancer) regeneratePicker() {
	if b.state == connectivity.TransientFailure {
		b.picker = NewErrPicker(b.mergeErrors())
		return
	}
	readySCs := make(map[balancer.SubConn]SubConnInfo)

	// Filter out all ready SCs from full subConn map.
	for addr, sc := range b.subConns.All() {
		if st, ok := b.scStates[sc]; ok && st == connectivity.Ready {
			readySCs[sc] = SubConnInfo{Address: addr}
		}

View on GitHub (pinned to 03255a9237)

Solutions

  1. Read BOTH wrapped values: the connection error first, the resolver error second.
  2. Fix whichever is the root cause (often the resolver error clears first once discovery heals).
  3. Capture a channelz snapshot to see per-SubConn state and the resolver's last error.
  4. Apply a client RetryPolicy so the RPC survives the transient dual failure.

Example fix

// Diagnose from the message itself, e.g.:
// 'last connection error: dial tcp: connect: connection refused; last resolver error: produced zero addresses'
// -> backends down AND resolver returning nothing. Fix service discovery first, then backend health.
Defensive patterns

Strategy: retry

Validate before calling

// Combine resolver + dial pre-flight checks from errors 92 and 93.
func preflight(target, dialAddr string) error {
    if err := checkResolves(target); err != nil { return err }
    return checkDial(dialAddr, 2*time.Second)
}

Try / catch

// Both layers failing usually means a broader outage; retry with backoff and
// surface a clear error to the caller after exhausting retries.
policy := retryPolicy(codes.Unavailable, 5, 1*time.Second)
return withRetry(ctx, policy, func() error { return stub.Do(ctx, req) })

Prevention

When it happens

Trigger: The balancer is in TransientFailure, connErr != nil and resolverErr != nil simultaneously. mergeErrors reaches the final return line.

Common situations: Resolver returns an error (DNS hiccup, xDS NACK) at the same time as existing backends are unreachable; a rolling deploy where the resolver hasn't refreshed and the old endpoints are dying; misconfigured custom resolver returning errors while TLS is also broken.

Related errors


AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07). Data as JSON: /api/errors/b83d3177da1e6052. Report an issue: GitHub.