Skip to content

proposal: Formalize multi-return values as tuple struct for fluent error handling #76025

@lakca

Description

@lakca

Proposal Details

Abstract

This proposal aims to formalize Go's multi-return values (e.g., (T, error)) as lightweight "tuple structs" at the language level. The primary goal is to enable fluent, chainable handling of multi-value results (especially error-aware workflows) while maintaining backward compatibility. This addresses the current friction in error handling—where manual error checks (if err != nil) are repetitive and non-fluent—without replacing existing patterns.

Background

Go's idiomatic error handling relies on multi-return values like (result, error). While explicit, this pattern often leads to repetitive code:

a, err := step1()
if err != nil {
    return err
}
b, err := step2(a)
if err != nil {
    return err
}
// ... and so on

Developers often work around this by manually implementing "result wrappers" (e.g., a Result[T] struct with Map/AndThen methods) to enable chaining. However, these wrappers are not standardized:

  • They require boilerplate conversion between (T, error) and the wrapper type.
  • They cannot seamlessly integrate with existing functions returning (T, error).
  • They lack interoperability across libraries, limiting their utility.

Formalizing multi-return values as tuple structs would make this pattern native to the language, enabling fluent error handling while retaining compatibility with existing code.

Proposal

Treat multi-return value sequences (e.g., (T, error), (int, string)) as built-in "tuple structs" with the following core characteristics:

  1. Tuple Struct Basics:
    A tuple struct is a nameless, fixed-size collection of values, defined by the types of its elements. For example, a function returning (int, error) is treated as returning tuple(int, error)—a tuple struct with two elements.

  2. Seamless Compatibility:

    • Existing code remains unchanged. Functions returning (T, error) are implicitly returning tuple(T, error).
    • Assignment via a, b := f() continues to work, now interpreted as "unpacking" the tuple struct into variables.
  3. Core Feature: Fluent Chaining for Error Handling:
    To enable fluent error-aware workflows, tuple structs with an error as their last element (the most common pattern, e.g., tuple(T, error)) would include built-in methods:

    • AndThen[U any](f func(T) (U, error)) (U, error):
      If the current tuple has no error, applies f to the first element; otherwise, propagates the error.
    • Map[U any](f func(T) U) (U, error):
      If no error, applies f to the first element (for non-error transformations); otherwise, propagates the error.
    • ...

    Example usage:

    func parse(s string) (int, error) { ... }
    func double(n int) (int, error) { ... }
    func addOne(n int) int { ... }
    
    // Fluent chain: parse → double → addOne
    val, err := parse("10").AndThen(double).Map(addOne)
    if err != nil { /* handle error */ }
  4. No New Syntax:
    Tuple structs reuse existing multi-return syntax. There is no need for new literals (e.g., (1, err) remains a return statement, not a "tuple literal").

Rationale

  • Fluent Error Handling: Reduces repetitive if err != nil checks by enabling chaining, while preserving Go's explicit error handling philosophy (errors are still unpacked at the end).
  • Standardization: Replaces fragmented manual Result wrappers with a language-native solution, ensuring interoperability across codebases.
  • Backward Compatibility: Existing code continues to work. Developers can adopt chaining incrementally.
  • Simplicity: Builds on familiar multi-return patterns instead of introducing new concepts like try/catch or heavyweight monads.

Compatibility

This proposal is fully backward compatible:

  • Existing functions returning (T, error) work with no changes.
  • Traditional if err != nil checks remain valid and are not deprecated.
  • Tuple structs are a隐形升级 (invisible upgrade) to multi-return values, not a breaking change.

Implementation Notes

  • The compiler would recognize tuple struct types (e.g., tuple(int, error)) as distinct from other types.
  • Methods like AndThen and Map would be implicitly available for tuples where the last element is error.
  • Unpacking logic (a, b := tuple) remains unchanged but is now semantically an operation on a tuple struct.

Future Extensions (Non-Core)

The core proposal focuses on error-aware chaining. If adopted, future extensions could include:

  • Allowing tuple structs to be stored in variables (e.g., var t tuple(int, error) = f()).
  • Supporting tuple structs as function parameters (e.g., func f(t tuple(int, error))).
  • Enabling unpacking in function calls (e.g., func g(a int, b error) { ... }; g(t...)).
  • Extending generic support for tuple structs (e.g., func Wrap[T any](f func() tuple(T, error))).

These extensions are not required for the core goal of improving error handling and can be evaluated separately.

Conclusion

Formalizing multi-return values as tuple structs with built-in chaining methods would make error handling more fluent and less repetitive, while preserving Go's simplicity and compatibility. By standardizing a pattern already used in manual workarounds, this proposal addresses a common pain point without disrupting existing code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions