grpc/grpc-go · error

failed to exit idle mode

Error message

failed to exit idle mode: %w

What it means

DialContext (deprecated, legacy) calls exitIdleMode() to eagerly start the resolver and load balancer (clientconn.go:302-304). If that fails (most commonly the resolver cannot start), DialContext wraps the underlying error and returns nil for the connection. The wrapped error usually comes from resolverWrapper.start().

Solutions

  1. Inspect the wrapped error (the %w chain) to find the root cause from the resolver.
  2. Verify the target scheme is registered (e.g., import the resolver package, or use a scheme like "dns:///" or "passthrough:///").
  3. Prefer grpc.NewClient over Dial/DialContext; NewClient starts in idle and reports resolver errors lazily, avoiding this eager-failure path.

Example fix

// before
cc, err := grpc.DialContext(ctx, "myscheme:///host", grpc.WithBlock())
// after
cc, err := grpc.NewClient("dns:///host", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil { log.Fatal(err) }
Defensive patterns

Strategy: try-catch

Try / catch

cc, err := grpc.DialContext(ctx, target, opts...)
if err != nil {
    if errors.Is(err, errConnClosing) { /* channel closed during dial */ }
    // unwrap to inspect the resolver error
    return fmt.Errorf("dial failed: %w", err)
}

Prevention

When it happens

Trigger: grpc.Dial or grpc.DialContext is used and exitIdleMode fails at clientconn.go:407 (resolverWrapper.start returns an error), which is then wrapped at clientconn.go:303.

Common situations: The dial target uses a scheme whose resolver builder is not registered; the resolver build itself errors (e.g., malformed URL, DNS resolver unavailable); a custom resolver returning an error from Build.

Related errors


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

Appendix: source

Thrown at clientconn.go:303

	// resolution on the client.
	opts = append([]DialOption{withDefaultScheme("passthrough"), WithLocalDNSResolution()}, opts...)
	cc, err := NewClient(target, opts...)
	if err != nil {
		return nil, err
	}

	// We start the channel off in idle mode, but kick it out of idle now,
	// instead of waiting for the first RPC.  This is the legacy behavior of
	// Dial.
	defer func() {
		if err != nil {
			cc.Close()
		}
	}()

	// This creates the name resolver, load balancer, etc.
	if err := cc.exitIdleMode(); err != nil {
		return nil, fmt.Errorf("failed to exit idle mode: %w", err)
	}
	cc.idlenessMgr.UnsafeSetNotIdle()

	// Return now for non-blocking dials.
	if !cc.dopts.block {
		return cc, nil
	}

	if cc.dopts.timeout > 0 {
		var cancel context.CancelFunc
		ctx, cancel = context.WithTimeout(ctx, cc.dopts.timeout)
		defer cancel()
	}
	defer func() {
		select {
		case <-ctx.Done():
			switch {
			case ctx.Err() == err:

View on GitHub (pinned to 0c51461d27)