Skip to main content
Version: v2.2.82 (latest)
Recipes · Error Handling

Error Handling Patterns.

Common patterns for handling errors functionally using fp-go's Result, Either, and IOResult types. Learn to chain, recover, and transform errors.

// Use Result
For simple success/failure with error.
// Use Either
For custom error types with context.
// Chain operations
Compose with Chain/Bind for pipelines.
01

Pattern 1: Basic error handling.

Use Result[A] when you need simple success/failure handling with Go's error type.

basic-result.go
package main

import (
  "errors"
  "fmt"
  
  R "github.com/IBM/fp-go/v2/result"
)

// Divide safely returns a Result
func safeDivide(a, b float64) R.Result[float64] {
  if b == 0 {
      return R.Left[float64](errors.New("division by zero"))
  }
  return R.Right[float64](a / b)
}

func main() {
  // Success case
  result1 := safeDivide(10, 2)
  fmt.Println(R.IsRight(result1)) // true
  
  // Error case
  result2 := safeDivide(10, 0)
  fmt.Println(R.IsLeft(result2)) // true
  
  // Extract value with default
  value := R.GetOrElse(func() float64 { return 0 })(result2)
  fmt.Println(value) // 0
}
02

Pattern 2: Chaining operations.

Chain multiple operations that can fail, short-circuiting on the first error.

chaining.go
package main

import (
  "errors"
  "fmt"
  "strconv"
  
  F "github.com/IBM/fp-go/v2/function"
  R "github.com/IBM/fp-go/v2/result"
)

// Parse string to int
func parseInt(s string) R.Result[int] {
  val, err := strconv.Atoi(s)
  if err != nil {
      return R.Left[int](err)
  }
  return R.Right[int](val)
}

// Validate positive number
func validatePositive(n int) R.Result[int] {
  if n <= 0 {
      return R.Left[int](errors.New("number must be positive"))
  }
  return R.Right[int](n)
}

// Double the number
func double(n int) R.Result[int] {
  return R.Right[int](n * 2)
}

func processNumber(s string) R.Result[int] {
  return F.Pipe3(
      parseInt(s),
      R.Chain(validatePositive),
      R.Chain(double),
  )
}

func main() {
  // Success: "5" -> 5 -> validate -> 10
  result1 := processNumber("5")
  fmt.Println(R.GetOrElse(func() int { return -1 })(result1)) // 10
  
  // Fails at parsing
  result2 := processNumber("abc")
  fmt.Println(R.IsLeft(result2)) // true
  
  // Fails at validation
  result3 := processNumber("-5")
  fmt.Println(R.IsLeft(result3)) // true
}
03

Pattern 3: Custom error types.

Use Either[E, A] when you need custom error types with more context.

custom-errors.go
package main

import (
  "fmt"
  
  E "github.com/IBM/fp-go/v2/either"
)

// Custom error types
type ValidationError struct {
  Field   string
  Message string
}

func (e ValidationError) Error() string {
  return fmt.Sprintf("%s: %s", e.Field, e.Message)
}

type User struct {
  Name  string
  Email string
  Age   int
}

// Validation functions
func validateName(name string) E.Either[ValidationError, string] {
  if len(name) < 2 {
      return E.Left[string](ValidationError{
          Field:   "name",
          Message: "must be at least 2 characters",
      })
  }
  return E.Right[ValidationError](name)
}

func validateEmail(email string) E.Either[ValidationError, string] {
  if len(email) < 5 || !contains(email, "@") {
      return E.Left[string](ValidationError{
          Field:   "email",
          Message: "invalid email format",
      })
  }
  return E.Right[ValidationError](email)
}

func validateAge(age int) E.Either[ValidationError, int] {
  if age < 18 {
      return E.Left[int](ValidationError{
          Field:   "age",
          Message: "must be at least 18",
      })
  }
  return E.Right[ValidationError](age)
}

// Create user with validation
func createUser(name, email string, age int) E.Either[ValidationError, User] {
  validName := validateName(name)
  if E.IsLeft(validName) {
      return E.Left[User](E.GetLeft(validName))
  }
  
  validEmail := validateEmail(email)
  if E.IsLeft(validEmail) {
      return E.Left[User](E.GetLeft(validEmail))
  }
  
  validAge := validateAge(age)
  if E.IsLeft(validAge) {
      return E.Left[User](E.GetLeft(validAge))
  }
  
  return E.Right[ValidationError](User{
      Name:  E.GetRight(validName),
      Email: E.GetRight(validEmail),
      Age:   E.GetRight(validAge),
  })
}
04

Pattern 4: Error recovery.

Provide fallback values or alternative computations when errors occur.

recovery.go
package main

import (
  "errors"
  "fmt"
  
  R "github.com/IBM/fp-go/v2/result"
)

// Try to get config from file
func getConfigFromFile() R.Result[string] {
  return R.Left[string](errors.New("file not found"))
}

// Fallback to environment variable
func getConfigFromEnv() R.Result[string] {
  return R.Left[string](errors.New("env var not set"))
}

// Final fallback to default
func getDefaultConfig() R.Result[string] {
  return R.Right[error]("default-config")
}

func getConfig() R.Result[string] {
  return R.OrElse(
      func() R.Result[string] {
          return R.OrElse(
              getConfigFromEnv,
          )(getConfigFromFile())
      },
  )(getDefaultConfig())
}

func main() {
  config := getConfig()
  value := R.GetOrElse(func() string { return "" })(config)
  fmt.Println(value) // "default-config"
}
05

Pattern 5: Mapping errors.

Transform error types while preserving the error state.

map-errors.go
package main

import (
  "fmt"
  
  E "github.com/IBM/fp-go/v2/either"
)

type DatabaseError struct {
  Code    int
  Message string
}

func (e DatabaseError) Error() string {
  return fmt.Sprintf("DB Error %d: %s", e.Code, e.Message)
}

type APIError struct {
  Status  int
  Message string
}

func (e APIError) Error() string {
  return fmt.Sprintf("API Error %d: %s", e.Status, e.Message)
}

// Database operation that returns DatabaseError
func queryDatabase() E.Either[DatabaseError, string] {
  return E.Left[string](DatabaseError{
      Code:    1001,
      Message: "connection timeout",
  })
}

// Convert DatabaseError to APIError
func toAPIError(dbErr DatabaseError) APIError {
  return APIError{
      Status:  500,
      Message: fmt.Sprintf("Database error: %s", dbErr.Message),
  }
}

func handleRequest() E.Either[APIError, string] {
  result := queryDatabase()
  return E.MapLeft(toAPIError)(result)
}

func main() {
  result := handleRequest()
  if E.IsLeft(result) {
      apiErr := E.GetLeft(result)
      fmt.Printf("%s\n", apiErr.Error())
      // API Error 500: Database error: connection timeout
  }
}
06

Pattern 6: Collecting errors.

When you need to collect all errors instead of short-circuiting.

collect-errors.go
package main

import (
  "fmt"
  "strings"
  
  E "github.com/IBM/fp-go/v2/either"
)

type ValidationErrors []string

func (ve ValidationErrors) Error() string {
  return strings.Join(ve, "; ")
}

func validateUserData(name, email string, age int) E.Either[ValidationErrors, bool] {
  var errors ValidationErrors
  
  if len(name) < 2 {
      errors = append(errors, "name too short")
  }
  
  if !contains(email, "@") {
      errors = append(errors, "invalid email")
  }
  
  if age < 18 {
      errors = append(errors, "age must be 18+")
  }
  
  if len(errors) > 0 {
      return E.Left[bool](errors)
  }
  
  return E.Right[ValidationErrors](true)
}

func main() {
  // Multiple validation errors
  result := validateUserData("A", "invalid", 15)
  
  if E.IsLeft(result) {
      errors := E.GetLeft(result)
      fmt.Printf("Validation failed: %s\n", errors.Error())
      // Validation failed: name too short; invalid email; age must be 18+
  }
}