Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result

Observing a Result

tap and tapError run a side effect and hand the result back unchanged. Use them to log without breaking a pipeline apart.

Verified
open System
open Reified.Result

type SignupError =
    | AgeNotANumber of string
    | AgeOutOfRange of int

let parseAge (age: string) : Result<int, SignupError> =
    match Int32.TryParse age with
    | true, value -> Ok value
    | false, _ -> Error(AgeNotANumber age)
Requires & types
Requires: open System
val parseAge: string -> Result<int,SignupError>

Without them

Logging mid-pipeline means naming the intermediate value and returning it again:

Fragment
let logged =
    let outcome = parseAge raw
    match outcome with
    | Ok age -> printfn "accepted %d" age
    | Error failure -> printfn "rejected: %A" failure
    outcome

With them

Verified
parseAge "abc"
|> Result.tap (fun age -> printfn "accepted %d" age)
|> Result.tapError (fun failure -> printfn "rejected: %A" failure)
Requires & types
Requires: open Reified.Result (packages: Reified.Result)
rejected: AgeNotANumber "abc"

The value returned is the original Error (AgeNotANumber "abc"). tap did not run, because the result was not Ok; tapError ran and returned its input untouched.

Both callbacks return unit; neither changes the result:

Fragment
Result.tap      : ('value -> unit) -> Result<'value, 'error> -> Result<'value, 'error>
Result.tapError : ('error -> unit) -> Result<'value, 'error> -> Result<'value, 'error>

At a boundary

At a boundary, where you want a record of what happened but the caller still gets the untouched result:

Fragment
let handleSignup raw =
    parseAge raw
    |> Result.tapError (fun failure -> logger.Warning("signup rejected: {Failure}", failure))
    |> Result.map buildAccount

failure can be AgeNotANumber raw, which keeps the caller's text verbatim, so logger.Warning(..., failure) writes that raw input out as-is. "Total" here means the callback never throws, not that its payload is safe to persist: when a field may hold secrets, credentials, or personal data, redact the failure before logging it.

Keep the effect small and total. An effect that throws will propagate out of tap, which defeats the purpose of working in Result, and because the exception escapes mid-pipeline, the result you were carrying is lost.