Golang Option Type — Option, Either, and Try for Go

Go’s if err != nil pattern is explicit, but it is also verbose. A function that chains three fallible operations needs three separate error checks, each with its own early return. The happy path drowns in boilerplate.

Libraries like IBM/fp-go bring Option and Either to Go, but they’re constrained by Go’s syntax — resulting in deeply nested generic calls and verbose type annotations. GALA solves this at the language level: Option[T], Either[A, B], and Try[T] are first-class sealed types with clean syntax, Map/FlatMap chaining, pattern matching, and full type inference. Errors propagate automatically through the chain; you only handle them when you are ready.


Option[T]: Some and None

Option[T] represents a value that may or may not exist. It is a sealed type with two variants:

sealed type Option[T any] {
    case Some(Value T)
    case None()
}

Use Option anywhere you would reach for nil in Go. It forces the caller to acknowledge the absence case.

Creating Options

val present = Some(42)          // Option[int] containing 42
val absent = None[int]()        // Option[int] with no value

Map and FlatMap

Transform the contained value without unwrapping. If the Option is None, the operation is skipped:

val opt = Some(10)
val doubled = opt.Map((x) => x * 2)         // Some(20)
val none = None[int]()
val still_none = none.Map((x) => x * 2)     // None[int]

// FlatMap chains operations that return Option
val result = opt.FlatMap((x) => if (x > 5) Some(x) else None[int]())

GetOrElse and OrElse

Extract the value with a fallback:

val value = opt.GetOrElse(0)                 // 42
val fallback = None[int]().GetOrElse(0)      // 0

// OrElse provides a fallback Option
val primary = None[string]()
val backup = Some("default")
val result = primary.OrElse(backup)          // Some("default")

Filter

Keep the value only if a predicate holds:

val positive = Some(42).Filter((x) => x > 0)    // Some(42)
val negative = Some(-1).Filter((x) => x > 0)    // None[int]

Side Effects

OnSome and OnNone run side effects and return the original Option for chaining:

val opt = Some(42)
opt.OnSome((v) => { Println(s"Found: $v") })
   .OnNone(() => { Println("Not found") })
   .Map((i) => i * 2)

Pattern Matching on Option

Because Option is a sealed type, matching is exhaustive — no case _ needed:

val msg = opt match {
    case Some(v) => s"Got: $v"
    case None()  => "Empty"
}

Key Methods

Method Description
IsDefined() / IsEmpty() Check the state
Get() Get value or panic
GetOrElse(default) Get value or return default
OrElse(alternative) Return this if Some, otherwise alternative
OnSome(f) / OnNone(f) Execute side-effect, return original
Map(f) Transform value if Some
FlatMap(f) Chain operations returning Option
Filter(predicate) Keep Some if predicate holds
ForEach(f) Apply procedure if nonempty

Either[A, B]: Left and Right

Either[A, B] represents a value that is one of two types. By convention, Left carries the error and Right carries the success value:

sealed type Either[A any, B any] {
    case Left(LeftValue A)
    case Right(RightValue B)
}

Creating Either Values

val success = Right[string, int](42)
val failure = Left[string, int]("not found")

Map and FlatMap on Right

Operations are biased toward Right. If the value is Left, the operation is skipped and the error propagates:

val result = Right[string, int](5)
    .Map((x) => x * 10)                                    // Right(50)
    .FlatMap((x) => Right[string, int](x + 1))             // Right(51)

val error = Left[string, int]("oops")
    .Map((x) => x * 10)                                    // Left("oops") — skipped

Chaining

Chain multiple operations. The first Left short-circuits the rest:

val chained = Right[string, int](3)
    .Map((x) => x * 10)
    .FlatMap((x) => Right[string, int](x + 1))
// Right(31)

Side Effects

OnRight and OnLeft run side effects and return the original Either:

val logged = Right[string, int](42)
    .OnRight((v) => { Println(s"Success: $v") })
    .OnLeft((e) => { Println(s"Error: $e") })
    .Map((x) => x * 2)

Pattern Matching on Either

Exhaustive matching — no default case needed:

val msg = result match {
    case Left(code)  => s"Error code: $code"
    case Right(s)    => "Result: " + s
}

Try[T]: Success and Failure

Try[T] wraps a computation that may succeed or fail with an error. It catches panics and turns them into Failure values:

sealed type Try[T any] {
    case Success(Value T)
    case Failure(Err error)
}

Creating Try Values

// Direct construction
val success = Success(42)
val failure = Failure[int](fmt.Errorf("oops"))

// Wrapping a failable computation — catches panics
val result = Try(() => riskyDivide(10, 0))     // Failure
val safe = Try(() => riskyDivide(10, 2))       // Success(5)

// Function reference sugar — no lambda needed for zero-arg functions
val dir = Try(os.TempDir)

Map and FlatMap

Transform success values. Failures propagate untouched:

val doubled = Success(21).Map((n) => n * 2)            // Success(42)
val chained = Success(10).FlatMap((n) => divide(n, 2)) // Success(5) or Failure

Recover and RecoverWith

Handle failures gracefully:

val recovered = Failure[int](fmt.Errorf("oops")).Recover((e) => 0)
// Success(0)

val recoveredWith = Failure[int](fmt.Errorf("oops")).RecoverWith((e) => Success(0))
// Success(0)

Side Effects

OnSuccess and OnFailure run side effects and return the original Try:

val logged = Success(42)
    .OnSuccess((n) => { Println(s"Got: $n") })
    .OnFailure((e) => { Println(s"Error: ${e.Error()}") })
    .Map((x) => x * 2)

Safe Extraction

val value = failure.GetOrElse(0)              // 0
val alternative = failure.OrElse(Success(100)) // Success(100)

Conversion

val opt = Success(42).ToOption()       // Some(42)
val either = Success(42).ToEither()    // Right(42)

Pattern Matching on Try

Exhaustive — no default case needed:

val msg = result match {
    case Success(n) => s"Got: $n"
    case Failure(e) => s"Error: ${e.Error()}"
}

Railway-Oriented Programming

Try enables elegant pipelines where errors short-circuit the chain:

func processOrder(id int) Try[Receipt] =
    fetchOrder(id)
        .FlatMap((o) => validateOrder(o))
        .FlatMap((o) => chargePayment(o))
        .FlatMap((o) => createReceipt(o))
        .RecoverWith((e) => {
            logError(e)
            return Failure[Receipt](e)
        })

Key Methods

Method Description
Try(f) Execute f, catch panics as Failure
IsSuccess() / IsFailure() Check the state
Get() Get value or panic
GetOrElse(default) Get value or return default
OrElse(alternative) Return this if Success, otherwise alternative
OnSuccess(f) / OnFailure(f) Execute side-effect, return original
Map(f) Transform value if Success
FlatMap(f) Chain operations returning Try
Filter(predicate) Keep Success if predicate holds
Recover(f) Recover from Failure with a value
RecoverWith(f) Recover from Failure with a new Try
ToOption() Convert to Option
ToEither() Convert to Either[error, T]

Chaining: Composing Operations into Pipelines

The real power of monadic error handling is composition. Instead of checking errors at every step, you build a pipeline and handle errors at the end:

val name = user.Name
    .Map((n) => strings.ToUpper(n))
    .GetOrElse("ANONYMOUS")

Compare this to the Go equivalent:

GALAGo
```gala val result = divide(10, 2) .Map((x) => x * 2) .FlatMap((x) => divide(x, 3)) .Recover((e) => 0) ``` ```go result, err := divide(10, 2) if err != nil { result = 0 } else { result = result * 2 result, err = divide(result, 3) if err != nil { result = 0 } } ```

The GALA version reads top-to-bottom. The happy path is the main path. Error handling is a single Recover at the end.


When Go’s Error Handling Is Fine

GALA’s monadic types are not always the right tool. Honest trade-offs:

The sweet spot for monadic error handling is multi-step pipelines where several operations can fail, and you want to keep the code linear and composable.


Further Reading