A client you can still use when you configured it wrong

In Go, constructors usually return a value and an error, but this binary contract fails for complex chat clients with many optional parts. The author proposes returning a usable client along with a detailed receipt of dropped settings, allowing developers to see which configurations were ignored an…

When building a chat client in Go, developers often face a dilemma: the client’s configuration includes many optional knobs, yet the traditional constructor pattern forces a binary success or failure. If one setting is incompatible, the entire construction fails, even though most of the client could still function. This article explains why the conventional approach is inadequate for chat clients and introduces a more flexible design that returns a usable client plus a detailed receipt of any dropped settings.

Why the Traditional Constructor Fails

The Go community has long embraced the NewX() (X, error) pattern. It works well for simple resources like files or sockets where construction either succeeds or it doesn’t. The contract is clear: if error is non‑nil, the returned value is unusable. Most developers rely on this rule, and it keeps code predictable.

Chat clients, however, are a different beast. They bundle a provider, a model, credentials, timeouts, endpoints, sampling controls, streaming options, tool support, a fallback chain, and a host of model‑specific knobs. Building one involves dozens of small decisions. Often, eleven of those decisions succeed while the twelfth—perhaps a temperature setting on a model that doesn’t support it—fails. The binary contract offers no way to express this partial success, forcing the library to choose between two unsatisfactory outcomes:

  • Return a fully constructed client and silently ignore the problematic setting.
  • Return an error and discard the entire client, even though most of it would have worked.

Both options misrepresent the reality of the situation and can lead to confusing bugs.

A Receipt‑Based Construction Pattern

To address this, the author proposes a new constructor contract: return a client and a receipt that lists any settings that were dropped, along with the reason. The client is still usable; the receipt provides actionable information. If a required setting such as a credential is missing, the constructor returns an error and no client. But for optional mismatches, the client is returned with a clear record of what didn’t apply.

The receipt is built using a custom DroppedSetting type:

type DroppedSetting struct {
    Names      []string
    Capability Capability
    Reason     string
}

Each DroppedSetting instance contains:

  • Names – the names of the configuration fields that were ignored.
  • Capability – the capability required by those fields (e.g., temperature support).
  • Reason – a short, human‑readable explanation of why the setting was dropped.

The constructor aggregates all such instances into a single error value that implements Go’s multi‑error unwrapping. This way, callers can inspect the receipt and decide whether to adjust their configuration or ignore the dropped settings.

Benefits and Trade‑Offs

Returning a usable client with a detailed receipt has several advantages:

  • Transparency – Callers see exactly what didn’t apply and why.
  • Flexibility – Developers can choose to ignore harmless mismatches instead of being forced to rewrite code.
  • Actionability – The receipt provides enough detail to fix the configuration without hunting through logs.

There are also trade‑offs. Some callers may ignore the receipt and proceed with a client that behaves unexpectedly, which the strict contract would have prevented. However, the strict contract never protected against a partially functional client; it only prevented a completely unusable one. By moving the decision to the caller, developers gain more control, but they must also read the receipt to avoid silent failures.

Implementing the Pattern in Go

Here’s a simplified example of how the constructor might look:

func NewChatClient(cfg Config) (*ChatClient, error) {
    client := &ChatClient{...}
    var dropped []DroppedSetting

    if !supportsTemperature(cfg.Model) {
        dropped = append(dropped, DroppedSetting{
            Names:      []string{"Temperature"},
            Capability: CapabilityTemperature,
            Reason:     "model does not support temperature",
        })
    }
    // ... handle other optional settings

    if len(dropped) > 0 {
        return client, NewDroppedSettingsError(dropped)
    }
    return client, nil
}

Callers can then handle the error like this:

client, err := NewChatClient(cfg)
if err != nil {
    if dropped, ok := err.(DroppedSettingsError); ok {
        for _, ds := range dropped.Settings {
            log.Printf("Setting %v was dropped: %s", ds.Names, ds.Reason)
        }
    } else {
        log.Fatalf("Failed to create client: %v", err)
    }
}

With this pattern, the client remains functional, and the developer is fully informed about any configuration mismatches.

Conclusion

Go’s traditional constructor contract works well for simple resources but falls short for complex, multi‑knob systems like chat clients. By returning a usable client along with a receipt of dropped settings, developers can maintain flexibility while still receiving clear, actionable feedback. This approach balances usability and safety, ensuring that partial failures are handled gracefully rather than forcing a hard crash or silently ignoring important configuration details.

Why it matters

The pattern empowers developers to build robust chat clients that can adapt to varying model capabilities without sacrificing usability, reducing debugging time and improving code quality.

Key points

  • Traditional Go constructors force binary success or failure, unsuitable for complex clients.
  • Partial failures can be reported via a receipt of dropped settings.
  • The receipt contains field names, required capabilities, and reasons.
  • Callers can inspect the receipt to adjust configurations or ignore mismatches.
  • The approach balances flexibility with transparency, avoiding silent failures.
  • Implementing multi‑error unwrapping keeps error handling idiomatic in Go.

Frequently asked questions

What happens if a required setting is missing?

The constructor returns an error and no client; the error indicates the missing requirement.

Can I ignore the receipt and proceed?

Yes, but you risk running a client that behaves unexpectedly. The receipt is there to help you make an informed decision.

Is this pattern compatible with existing Go code?

Yes, it follows Go’s error handling conventions and can be integrated into existing libraries with minimal changes.

Reporting drawn from

More from World

Felo News, House 42, Bridge Colony, Kot Lakhpat, Lahore, Pakistan
+92 308 4354717 · felopronews@gmail.com