Skip to content

F# Guide — Result Types: Composable Error Handling

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about F# Guide. We cover key concepts, practical examples, and best practices to help you master this topic.

F# Result types provide a type-safe way to handle operations that can either succeed (Ok) or fail (Error), with built-in functions for chaining, mapping, and composing fallible operations.

What You'll Learn

  • Defining and using Result types
  • Pattern matching on Ok and Error
  • Result.bind for chaining
  • Converting between Result and Option
  • Error handling patterns

Why It Matters

Result types make error handling explicit and composable, eliminating try-catch blocks for expected failures. Durga Antivirus Pro uses Result types for scan operation results.

Real-World Use

File I/O operations, network requests, data validation, Parsing, and any operation that can predictably fail.

flowchart LR
    A["Result Types"] --> B["Ok & Error"]
    B --> C["Pattern Matching"]
    C --> D["Result.bind"]
    D --> E["Error Handling"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Creating Results

// Success
let success = Ok 42

// Failure
let failure: Result<int, string> = Error "Something went wrong"

// In a function
let divide x y =
    if y = 0 then Error "Division by zero"
    else Ok (x / y)

Pattern Matching

let handleResult result =
    match result with
    | Ok value -> sprintf "Success: %d" value
    | Error msg -> sprintf "Error: %s" msg

handleResult (Ok 42)   // "Success: 42"
handleResult (Error "fail")  // "Error: fail"

Result.bind (Chaining)

let tryParseInt (s: string) =
    match System.Int32.TryParse(s) with
    | (true, n) -> Ok n
    | _ -> Error "Invalid integer"

let trySqrt x =
    if x >= 0 then Ok (sqrt x)
    else Error "Cannot sqrt negative"

let result =
    Ok "16"
    |> Result.bind tryParseInt
    |> Result.bind trySqrt
// Ok 4.0

let failResult =
    Ok "abc"
    |> Result.bind tryParseInt
    |> Result.bind trySqrt
// Error "Invalid integer"

Result.map

// Transform the success value
Ok 5
|> Result.map (fun x -> x * 2)
|> Result.map (fun x -> sprintf "Result: %d" x)
// Ok "Result: 10"

// Map applies only to Ok, leaves Error unchanged
Error "fail"
|> Result.map (fun x -> x * 2)
// Error "fail"

Result with Computation Expressions

// F# 4.1+ supports Result computation expressions
let result = result {
    let! a = tryParseInt "16"
    let! b = trySqrt (float a)
    return b
}

Custom Error Types

type ValidationError =
    | EmptyField of string
    | InvalidFormat of string
    | OutOfRange of string * int * int

type ValidationResult<'T> = Result<'T, ValidationError list>

let validateName name =
    if System.String.IsNullOrWhiteSpace(name) then
        Error [EmptyField "Name"]
    else
        Ok name

let validateAge age =
    match System.Int32.TryParse(age) with
    | (true, n) when n >= 0 && n <= 150 -> Ok n
    | (true, _) -> Error [OutOfRange("Age", 0, 150)]
    | _ -> Error [InvalidFormat "Age"]

Converting Between Option and Result

// Option to Result
let optionToResult errorValue opt =
    match opt with
    | Some v -> Ok v
    | None -> Error errorValue

// Result to Option
let resultToOption result =
    match result with
    | Ok v -> Some v
    | Error _ -> None

// Using built-in functions
let result1 = Ok 42 |> Result.toOption  // Some 42
let result2 = Error "fail" |> Result.toOption  // None

Common Mistakes

1. Using exceptions for expected failures

Use Result for expected failures (validation, missing data). Use exceptions for unexpected errors.

2. Ignoring the Error case

Always handle both Ok and Error. The compiler warns about non-exhaustive matches.

3. Using string errors

Use discriminated unions or custom types for errors. String errors are hard to handle programmatically.

4. Deeply nested error handling

Use Result.bind or computation expressions to flatten nested match expressions.

5. Not propagating errors

Handle errors at the appropriate level. Don't swallow errors unless you have a specific reason.

Practice Questions

1. What does Result<'T, 'Error> represent? An operation that returns either Ok with value of type 'T or Error with value of type 'Error.

2. How does Result.bind differ from Result.map? bind applies a function that returns Result (possibly failing). map applies a function that returns a plain value (cannot fail).

3. Why use custom error types? Custom error types allow callers to handle specific errors differently, unlike string messages.

Challenge: Write a function that validates and processes a user registration form using Result types.

FAQ

{{< faq question="Should I use Result or exceptions?" >}} Use Result for expected, recoverable failures (validation, network errors). Use exceptions for unexpected, unrecoverable errors (out of memory, assertion failures). {{< /faq >}}

{{< faq question="Can I use Result with async?" >}} Yes. Combine Result with Async or Task: Async<Result<'T, 'Error>> is a common pattern for async operations that can fail. {{< /faq >}}

{{< faq question="How do I collect multiple errors?" >}} Use a list of errors as the error type: Result<'T, Error list>. Functions can accumulate multiple validation errors before failing. {{< /faq >}}

{{< faq question="Is Result the same as Either in other languages?" >}} Yes, Result is F#'s implementation of the Either monad, specialized with Ok and Error cases for success/failure semantics. {{< /faq >}}

{{< faq question="What is result computation expression?" >}} A computation expression (CE) for Result that lets you use let! to bind Ok values and short-circuit on Error, similar to Haskell's do notation. {{< /faq >}}

Mini Project

Build a data validation pipeline using Result types:

type User = { Name: string; Email: string; Age: int }

type ValidationError =
    | NameRequired
    | InvalidEmail
    | AgeOutOfRange

let validateName name =
    if System.String.IsNullOrWhiteSpace(name) then Error NameRequired
    else Ok name

let validateEmail email =
    if email.Contains("@") then Ok email
    else Error InvalidEmail

let validateAge age =
    if age >= 18 && age <= 120 then Ok age
    else Error AgeOutOfRange

let validateUser name email age = result {
    let! n = validateName name
    let! e = validateEmail email
    let! a = validateAge age
    return { Name = n; Email = e; Age = a }
}

What's Next

Now that you understand Result types, explore modules for organizing F# code.

Topic Description Link
F# Modules Code organization {{< ref "14-modules" >}}
F# Collections Collection operations {{< ref "15-collections" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro