grpc/grpc-go · error

failed to start resolver

Error message

failed to start resolver: %w

What it means

exitIdleMode starts the resolver wrapper (clientconn.go:407). If resolverWrapper.start() fails, the channel is put into TRANSIENT_FAILURE and the error is wrapped as "failed to start resolver: %w" (clientconn.go:416). This surfaces from DialContext (eager) and from the first RPC (lazy, via NewClient).

Solutions

  1. Unwrap the error to see the resolver-specific failure message.
  2. Confirm the scheme in the target maps to a registered resolver (import its package or use a built-in like dns/passthrough).
  3. Use grpc.NewClient and check the error from the first RPC to get the lazy, unwrapped resolver error, or switch to a known scheme.

Example fix

// before
cc, err := grpc.NewClient("xds:///wrong") // xds not registered / misconfigured
// after: register/fix scheme, or use dns
cc, err := grpc.NewClient("dns:///my.backend:443", grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
Defensive patterns

Strategy: try-catch

Try / catch

cc, err := grpc.NewClient(target, opts...)
if err != nil { return err }
// first RPC surfaces resolver errors lazily; alternatively call Connect()
if err := someStub.Call(ctx, req); err != nil {
    if strings.Contains(err.Error(), "failed to start resolver") { /* resolver config issue */ }
}

Prevention

When it happens

Trigger: The resolver builder's Build returns an error; the target URL is malformed; the resolver scheme is unregistered so no builder is found; a custom resolver fails to initialize.

Common situations: Target string like "foo:///host" where "foo" resolver is not registered; missing import of the resolver package; invalid target URL that fails url.Parse inside the resolver; DNS resolver failing at construction in restricted environments.

Related errors


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

Appendix: 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 0c51461d27)