Skip to main content
Version: v2.2.82 (latest)
Reference · Core Type

IOEither

Combine lazy evaluation (IO) with custom error handling (Either). IOEither[E, A] represents a synchronous computation with side effects that may fail with custom error type E.

01

Overview

IOEither combines two powerful abstractions:

type_definition.go
package ioeither

// IOEither is IO of Either
type IOEither[E, A any] = IO[Either[E, A]]
// Which expands to: func() Either[E, A]

When to Use

Use IOEither WhenUse IOResult When
Custom error types with rich informationStandard Go error interface
Domain-specific error handlingLibrary interoperability
Type-level error distinctionSimpler type signatures
Migration from v1 (used IOEither extensively)Working with existing Go code
02

Core API

Constructors

FunctionSignatureDescription
Rightfunc Right[E, A any](value A) IOEither[E, A]Create successful IOEither
Leftfunc Left[E, A any](err E) IOEither[E, A]Create failed IOEither
Offunc Of[E, A any](value A) IOEither[E, A]Alias for Right
TryCatchfunc TryCatch[E, A any](f func() (A, E)) IOEither[E, A]From function returning tuple
FromIOfunc FromIO[E, A any](io IO[A]) IOEither[E, A]Lift IO to IOEither
FromEitherfunc FromEither[E, A any](e Either[E, A]) IOEither[E, A]Lift Either to IOEither

Transformations

FunctionSignatureDescription
Mapfunc Map[E, A, B any](f func(A) B) func(IOEither[E, A]) IOEither[E, B]Transform success value
MapLeftfunc MapLeft[A, E1, E2 any](f func(E1) E2) func(IOEither[E1, A]) IOEither[E2, A]Transform error value
BiMapfunc BiMap[E1, E2, A, B any](fe func(E1) E2, fa func(A) B) func(IOEither[E1, A]) IOEither[E2, B]Transform both sides
Chainfunc Chain[E, A, B any](f func(A) IOEither[E, B]) func(IOEither[E, A]) IOEither[E, B]Sequence operations

Combining

FunctionSignatureDescription
Apfunc Ap[E, A, B any](fa IOEither[E, A]) func(IOEither[E, func(A) B]) IOEither[E, B]Apply wrapped function
SequenceArrayfunc SequenceArray[E, A any]([]IOEither[E, A]) IOEither[E, []A]All-or-nothing
TraverseArrayfunc TraverseArray[E, A, B any](f func(A) IOEither[E, B]) func([]A) IOEither[E, []B]Map and sequence

Pattern Matching

FunctionSignatureDescription
Foldfunc Fold[E, A, B any](onLeft func(E) IO[B], onRight func(A) IO[B]) func(IOEither[E, A]) IO[B]Extract to IO
Matchfunc Match[E, A, B any](onLeft func(E) IOEither[E, B], onRight func(A) IOEither[E, B]) func(IOEither[E, A]) IOEither[E, B]Pattern match

Utilities

FunctionSignatureDescription
ChainFirstfunc ChainFirst[E, A, B any](f func(A) IOEither[E, B]) func(IOEither[E, A]) IOEither[E, A]Side effect, keep value
Altfunc Alt[E, A any](second IOEither[E, A]) func(IOEither[E, A]) IOEither[E, A]Fallback on Left
OrElsefunc OrElse[E, A any](f func(E) IOEither[E, A]) func(IOEither[E, A]) IOEither[E, A]Lazy fallback
03

Usage Examples

Basic Operations

basic.go
package main

import (
  "fmt"
  IOE "github.com/IBM/fp-go/v2/ioeither"
)

type AppError struct {
  Code    int
  Message string
}

func main() {
  // Create IOEither values
  success := IOE.Right[AppError](42)
  failure := IOE.Left[int](AppError{
      Code:    404,
      Message: "Not found",
  })
  
  // Execute
  result := success()  // Either[AppError, int] = Right(42)
  result = failure()   // Either[AppError, int] = Left(AppError{...})
}

File Operations

file_ops.go
package main

import (
  "os"
  IOE "github.com/IBM/fp-go/v2/ioeither"
)

type FileError struct {
  Path    string
  Message string
}

func readConfig(path string) IOE.IOEither[FileError, Config] {
  return IOE.TryCatch(func() (Config, FileError) {
      data, err := os.ReadFile(path)
      if err != nil {
          return Config{}, FileError{
              Path:    path,
              Message: err.Error(),
          }
      }
      
      var cfg Config
      if err := json.Unmarshal(data, &cfg); err != nil {
          return Config{}, FileError{
              Path:    path,
              Message: "invalid JSON: " + err.Error(),
          }
      }
      
      return cfg, FileError{}
  })
}

func main() {
  result := readConfig("config.json")()
  // Either[FileError, Config]
}

Chaining Operations

chaining.go
package main

import (
  IOE "github.com/IBM/fp-go/v2/ioeither"
  F "github.com/IBM/fp-go/v2/function"
)

type ValidationError struct {
  Field   string
  Message string
}

func fetchUser(id string) IOE.IOEither[ValidationError, User] {
  return IOE.TryCatch(func() (User, ValidationError) {
      // Fetch from database
      user, err := db.FindUser(id)
      if err != nil {
          return User{}, ValidationError{
              Field:   "id",
              Message: err.Error(),
          }
      }
      return user, ValidationError{}
  })
}

func validateUser(user User) IOE.IOEither[ValidationError, User] {
  if user.Age < 18 {
      return IOE.Left[User](ValidationError{
          Field:   "age",
          Message: "must be 18 or older",
      })
  }
  return IOE.Right[ValidationError](user)
}

func saveUser(user User) IOE.IOEither[ValidationError, User] {
  return IOE.TryCatch(func() (User, ValidationError) {
      err := db.SaveUser(user)
      if err != nil {
          return User{}, ValidationError{
              Field:   "save",
              Message: err.Error(),
          }
      }
      return user, ValidationError{}
  })
}

func processUser(id string) IOE.IOEither[ValidationError, User] {
  return F.Pipe3(
      fetchUser(id),
      IOE.Chain(validateUser),
      IOE.Chain(saveUser),
  )
}

func main() {
  result := processUser("user-123")()
  // Either[ValidationError, User]
}

Error Context

error_context.go
package main

import (
  IOE "github.com/IBM/fp-go/v2/ioeither"
  F "github.com/IBM/fp-go/v2/function"
)

func enrichError(operation string) func(AppError) AppError {
  return func(err AppError) AppError {
      return AppError{
          Code:    err.Code,
          Message: fmt.Sprintf("%s: %s", operation, err.Message),
      }
  }
}

func processData(id string) IOE.IOEither[AppError, Data] {
  return F.Pipe3(
      fetchData(id),
      IOE.MapLeft(enrichError("fetch")),
      IOE.Chain(validateData),
      IOE.MapLeft(enrichError("validate")),
  )
}
04

Common Patterns

Pattern 1: Resource Management

resource.go
package main

import (
  "os"
  IOE "github.com/IBM/fp-go/v2/ioeither"
)

func processFile(path string) IOE.IOEither[AppError, Data] {
  return IOE.Bracket(
      // Acquire
      func() IOE.IOEither[AppError, *os.File] {
          return IOE.TryCatch(func() (*os.File, AppError) {
              f, err := os.Open(path)
              if err != nil {
                  return nil, AppError{Code: 500, Message: err.Error()}
              }
              return f, AppError{}
          })
      },
      // Use
      func(f *os.File) IOE.IOEither[AppError, Data] {
          return parseData(f)
      },
      // Release (always runs)
      func(f *os.File, _ IOE.IOEither[AppError, Data]) IOE.IOEither[AppError, unit.Unit] {
          f.Close()
          return IOE.Right[AppError](unit.Unit{})
      },
  )
}

Pattern 2: Fallback on Error

fallback.go
package main

import (
  IOE "github.com/IBM/fp-go/v2/ioeither"
)

func fetchWithFallback(id string) IOE.IOEither[AppError, Data] {
  return F.Pipe1(
      fetchFromCache(id),
      IOE.OrElse(func(err AppError) IOE.IOEither[AppError, Data] {
          // Fallback to database on cache miss
          return fetchFromDB(id)
      }),
  )
}

Comparison: IOEither vs IOResult

Before
ioeither_for_error.go
// ❌ Don't use IOEither[error, A]
func readFile(path string) IOE.IOEither[error, []byte] {
  // Use IOResult instead
}
After
ioeither_for_custom.go
// ✅ Use IOEither for custom errors
func readFile(path string) IOE.IOEither[FileError, []byte] {
  // Rich error information
}

// ✅ Use IOResult for standard errors
func readFile(path string) IOR.IOResult[[]byte] {
  // Simpler, idiomatic
}