grpc/grpc-go · error

failed to start resolver: %w

Error message

failed to start resolver: %w

What it means

Inside exitIdleMode (clientconn.go:407-417), the channel calls resolverWrapper.start(). If starting the resolver fails, the channel is moved to TransientFailure with an erroring picker and the build error is wrapped as "failed to start resolver". Any subsequent RPC, or the Dial/DialContext that triggered exit, gets this error.

Source

Thrown at clientconn.go:416

	cc.mu.Unlock()

	// Set state to CONNECTING before building the name resolver
	// so the channel does not remain in IDLE.
	cc.csMgr.updateState(connectivity.Connecting)

	// This needs to be called without cc.mu because this builds a new resolver
	// which might update state or report error inline, which would then need to
	// acquire cc.mu.
	if err := cc.resolverWrapper.start(); err != nil {
		// If resolver creation fails, treat it like an error reported by the
		// resolver before any valid updates. Set channel's state to
		// TransientFailure, and set an erroring picker with the resolver build
		// error, which will returned as part of any subsequent RPCs.
		logger.Warningf("Failed to start resolver: %v", err)
		cc.csMgr.updateState(connectivity.TransientFailure)
		cc.mu.Lock()
		cc.updateResolverStateAndUnlock(resolver.State{}, err)
		return fmt.Errorf("failed to start resolver: %w", err)
	}

	cc.addTraceEvent("exiting idle mode")
	return nil
}

// initIdleStateLocked initializes common state to how it should be while idle.
func (cc *ClientConn) initIdleStateLocked() {
	cc.resolverWrapper = newCCResolverWrapper(cc)
	cc.balancerWrapper = newCCBalancerWrapper(cc)
	cc.firstResolveEvent = grpcsync.NewEvent()
	// cc.conns == nil is a proxy for the ClientConn being closed. So, instead
	// of setting it to nil here, we recreate the map. This also means that we
	// don't have to do this when exiting idle mode.
	cc.conns = make(map[*addrConn]struct{})
}

// enterIdleMode puts the channel in idle mode, and as part of it shuts down the

View on GitHub (pinned to 03255a9237)

Solutions

  1. Check the inner %w error for the resolver's Build failure message and fix that (register the builder, fix target scheme).
  2. Add the missing blank import for the resolver package (e.g. _ "google.golang.org/grpc/xds/googledirectpath") if it self-registers in init().
  3. Validate the target URL scheme before dialing.

Example fix

// before
conn, _ := grpc.NewClient("dns_nonexistent:///svc")
// after
import _ "google.golang.org/grpc/internal/resolver/dns"
conn, _ := grpc.NewClient("dns:///svc:443")
Defensive patterns

Strategy: validation

Validate before calling

// Ensure a builder exists for the scheme (or default scheme) before dial.
func ensureResolver(scheme string) error {
    for _, s := range resolver.GetSchemes() { if s == scheme { return nil } }
    return fmt.Errorf("no resolver registered for scheme %q", scheme)
}

Prevention

When it happens

Trigger: The resolver builder's Build() returns an error when the channel leaves idle mode — e.g. a custom resolver failing, or no builder registered for the target's scheme. Triggered on first RPC (NewClient) or immediately (Dial/DialContext).

Common situations: Target uses a scheme with no registered resolver; a custom resolver builder panics/errors; the resolver plugin isn't imported (blank import missing) so its init() never registers.

Related errors


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