Header menu logo AsyncWriterResult
#r "../src/AsyncWriterResult/bin/Release/net8.0/publish/AsyncWriterResult.dll"

AsyncWriterResult

Combine async workflows with logging and error handling in F#. Track what's happening in your async code without the mess.

Quick Start

open AsyncWriterResult

Writing async code that logs its work

Let's say you're fetching data from an API and want to track what's happening:

type User = { Id: int; Name: string }

let fetchUserWithLogs userId =
    asyncWriter {
        do! Writer.write $"[INFO] Fetching user {userId}"

        // Simulate API call
        let! user =
            async {
                do! Async.Sleep 200 // network delay
                return { Id = userId; Name = "Alice" }
            }

        do! Writer.write $"[INFO] Got user: {user.Name}"
        return user
    }

// Run it and see what happened
let user, fetchLogs = fetchUserWithLogs 123 |> Async.RunSynchronously |> Writer.run

printfn $"User: %A{user}"
printfn $"What happened: %A{fetchLogs}"
User: { Id = 123
  Name = "Alice" }
What happened: ["[INFO] Fetching user 123"; "[INFO] Got user: Alice"]

When things can fail

Real code fails. Here's how to handle errors while keeping the logs:

let parseConfigFile filename =
    asyncWriterResult {
        do! Writer.write $"[INFO] Reading config from {filename}"

        // Check if file exists
        if not (System.IO.File.Exists filename) then
            do! Writer.write $"[ERROR] File not found: {filename}"
            return! Error $"Config file '{filename}' doesn't exist"

        // Try to read and parse
        let! content = async { return System.IO.File.ReadAllText filename }

        do! Writer.write $"[INFO] Read {content.Length} characters"

        // Parse JSON (simplified)
        if content.StartsWith "{" then
            do! Writer.write "[INFO] Valid JSON detected"
            return content
        else
            do! Writer.write "[ERROR] Invalid JSON format"
            return! Error "Invalid config format"
    }

// Try with a missing file
let configResult, configLogs =
    parseConfigFile "missing.json" |> Async.RunSynchronously |> Writer.run

match configResult with
| Ok config -> printfn $"Config loaded: %s{config}"
| Error(msg: string) -> printfn $"Failed: %s{msg}"

printfn $"Log trail:\n%A{configLogs}"
Failed: Config file 'missing.json' doesn't exist
Log trail:
["[INFO] Reading config from missing.json";
 "[ERROR] File not found: missing.json"]

Running things in parallel

Fetch multiple things at once with and! - they run in parallel but logs stay organized:

let checkServiceHealth (url: string) =
    async {
        do! Async.Sleep 100 // simulate network call

        return if url.Contains "api" then "healthy" else "degraded"
    }

let healthCheck () =
    asyncWriterResult {
        do! Writer.write "[START] Health check initiated"

        let! apiStatus = checkServiceHealth "https://api.example.com"
        and! dbStatus = checkServiceHealth "https://db.example.com"
        and! cacheStatus = checkServiceHealth "https://cache.example.com"

        do! Writer.write $"[STATUS] API: {apiStatus}"
        do! Writer.write $"[STATUS] Database: {dbStatus}"
        do! Writer.write $"[STATUS] Cache: {cacheStatus}"

        let allHealthy =
            apiStatus = "healthy" && dbStatus = "healthy" && cacheStatus = "healthy"

        if allHealthy then
            do! Writer.write "[OK] All systems operational"
            return "All systems GO"
        else
            do! Writer.write "[WARN] Some services degraded"
            return "Partial outage"
    }

let healthResult, healthLogs =
    healthCheck () |> Async.RunSynchronously |> Writer.run

match healthResult with
| Ok status -> printfn $"Status: %s{status}"
| Error(_: string) -> printfn "Health check failed"

printfn $"\nHealth check log:\n%A{healthLogs}"
Status: Partial outage

Health check log:
["[START] Health check initiated"; "[STATUS] API: healthy";
 "[STATUS] Database: degraded"; "[STATUS] Cache: degraded";
 "[WARN] Some services degraded"]

Processing lists with detailed logging

Track what happens to each item when processing collections:

type Order = { OrderId: string; Amount: decimal }

let processOrders orders =
    asyncWriterResult {
        do! Writer.write $"[START] Processing {List.length orders} orders"
        let mutable total = 0m

        for order in orders do
            do! Writer.write $"[PROCESS] Order {order.OrderId}: ${order.Amount}"

            // Validate
            if order.Amount <= 0m then
                do! Writer.write $"[SKIP] Invalid amount for {order.OrderId}"
            else
                total <- total + order.Amount
                do! Writer.write $"[OK] Added ${order.Amount} (running total: ${total})"

        do! Writer.write $"[COMPLETE] Processed batch. Total: ${total}"
        return total
    }

let orders =
    [ { OrderId = "ORD-001"; Amount = 99.99m }
      { OrderId = "ORD-002"; Amount = 0m } // invalid
      { OrderId = "ORD-003"
        Amount = 150.00m } ]

let orderResult, orderLogs =
    processOrders orders |> Async.RunSynchronously |> Writer.run

match orderResult with
| Ok total -> printfn $"Total processed: $%.2f{total}"
| Error(e: string) -> printfn $"Processing failed: %s{e}"

printfn "\nProcessing log:"
orderLogs |> List.iter (printfn "  %s")
Total processed: $249.99

Processing log:
  [START] Processing 3 orders
  [PROCESS] Order ORD-001: $99.99
  [OK] Added $99.99 (running total: $99.99)
  [PROCESS] Order ORD-002: $0
  [SKIP] Invalid amount for ORD-002
  [PROCESS] Order ORD-003: $150.00
  [OK] Added $150.00 (running total: $249.99)
  [COMPLETE] Processed batch. Total: $249.99

Composing operations

Chain operations together - logs flow through automatically:

let validateInput (input: string) =
    asyncWriter {
        do! Writer.write $"[VALIDATE] Checking '{input}'"

        if String.length input > 3 then
            do! Writer.write "[VALIDATE] Input valid"
            return input.ToUpper()
        else
            do! Writer.write "[VALIDATE] Too short!"
            return ""
    }

let processData (data: string) =
    asyncWriter {
        do! Writer.write $"[PROCESS] Working with '{data}'"
        let result = data.Replace("TEST", "PROD")
        do! Writer.write $"[PROCESS] Transformed to '{result}'"
        return result
    }

let pipeline input =
    input |> validateInput |> AsyncWriter.bind processData

let finalResult, pipelineLogs =
    pipeline "test_data" |> Async.RunSynchronously |> Writer.run

printfn $"Result: %s{finalResult}"
printfn "\nPipeline trace:"
pipelineLogs |> List.iter (printfn "  %s")
Result: PROD_DATA

Pipeline trace:
  [VALIDATE] Checking 'test_data'
  [VALIDATE] Input valid
  [PROCESS] Working with 'TEST_DATA'
  [PROCESS] Transformed to 'PROD_DATA'
Multiple items
module AsyncWriterResult from AsyncWriterResult

--------------------
namespace AsyncWriterResult

--------------------
type AsyncWriterResult<'ok,'error,'log> = Async<Writer<'log,Result<'ok,'error>>>
type User = { Id: int Name: string }
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
val fetchUserWithLogs: userId: int -> AsyncWriter<string,User>
val userId: int
val asyncWriter: AsyncWriterBuilder
Multiple items
union case Writer.Writer: (unit -> 'item * 'log list) -> Writer<'log,'item>

--------------------
module Writer from AsyncWriterResult

--------------------
type Writer<'log,'item> = | Writer of (unit -> 'item * 'log list)
val write: x: 'a -> Writer<'a,unit>
val user: User
val async: AsyncBuilder
Multiple items
module Async from AsyncWriterResult

--------------------
type Async = static member AsBeginEnd: computation: ('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit) static member AwaitEvent: event: IEvent<'Del,'T> * ?cancelAction: (unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate) static member AwaitIAsyncResult: iar: IAsyncResult * ?millisecondsTimeout: int -> Async<bool> static member AwaitTask: task: Task<'T> -> Async<'T> + 1 overload static member AwaitWaitHandle: waitHandle: WaitHandle * ?millisecondsTimeout: int -> Async<bool> static member CancelDefaultToken: unit -> unit static member Catch: computation: Async<'T> -> Async<Choice<'T,exn>> static member Choice: computations: Async<'T option> seq -> Async<'T option> static member FromBeginEnd: beginAction: (AsyncCallback * obj -> IAsyncResult) * endAction: (IAsyncResult -> 'T) * ?cancelAction: (unit -> unit) -> Async<'T> + 3 overloads static member FromContinuations: callback: (('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T> ...

--------------------
type Async<'T>
static member Async.Sleep: dueTime: System.TimeSpan -> Async<unit>
static member Async.Sleep: millisecondsDueTime: int -> Async<unit>
val fetchLogs: string list
static member Async.RunSynchronously: computation: Async<'T> * ?timeout: int * ?cancellationToken: System.Threading.CancellationToken -> 'T
val run: Writer<'w,'t> -> 't * 'w list
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
val parseConfigFile: filename: string -> Async<Writer<string,Result<string,string>>>
val filename: string
val asyncWriterResult: AsyncWriterResultBuilder
namespace System
namespace System.IO
type File = static member AppendAllLines: path: string * contents: IEnumerable<string> -> unit + 1 overload static member AppendAllLinesAsync: path: string * contents: IEnumerable<string> * encoding: Encoding * ?cancellationToken: CancellationToken -> Task + 1 overload static member AppendAllText: path: string * contents: string -> unit + 1 overload static member AppendAllTextAsync: path: string * contents: string * encoding: Encoding * ?cancellationToken: CancellationToken -> Task + 1 overload static member AppendText: path: string -> StreamWriter static member Copy: sourceFileName: string * destFileName: string -> unit + 1 overload static member Create: path: string -> FileStream + 2 overloads static member CreateSymbolicLink: path: string * pathToTarget: string -> FileSystemInfo static member CreateText: path: string -> StreamWriter static member Decrypt: path: string -> unit ...
<summary>Provides static methods for the creation, copying, deletion, moving, and opening of a single file, and aids in the creation of <see cref="T:System.IO.FileStream" /> objects.</summary>
System.IO.File.Exists(path: string) : bool
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val content: string
System.IO.File.ReadAllText(path: string) : string
System.IO.File.ReadAllText(path: string, encoding: System.Text.Encoding) : string
val configResult: Result<string,string>
val configLogs: string list
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
val config: string
val msg: string
val checkServiceHealth: url: string -> Async<string>
val url: string
System.String.Contains(value: string) : bool
System.String.Contains(value: char) : bool
System.String.Contains(value: string, comparisonType: System.StringComparison) : bool
System.String.Contains(value: char, comparisonType: System.StringComparison) : bool
val healthCheck: unit -> Async<Writer<string,Result<string,'a>>>
val apiStatus: string
val dbStatus: string
val cacheStatus: string
val allHealthy: bool
val healthResult: Result<string,string>
val healthLogs: string list
val status: string
type Order = { OrderId: string Amount: decimal }
Multiple items
val decimal: value: 'T -> decimal (requires member op_Explicit)

--------------------
type decimal = System.Decimal

--------------------
type decimal<'Measure> = decimal
val processOrders: orders: Order list -> Async<Writer<string,Result<decimal,'a>>>
val orders: Order list
Multiple items
module List from AsyncWriterResult

--------------------
module List from Microsoft.FSharp.Collections

--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T member IsEmpty: bool member Item: index: int -> 'T with get ...
val length: list: 'T list -> int
val mutable total: decimal
val order: Order
Order.OrderId: string
Order.Amount: decimal
val orderResult: Result<decimal,string>
val orderLogs: string list
val total: decimal
val e: string
val iter: action: ('T -> unit) -> list: 'T list -> unit
val validateInput: input: string -> AsyncWriter<string,string>
val input: string
module String from Microsoft.FSharp.Core
val length: str: string -> int
System.String.ToUpper() : string
System.String.ToUpper(culture: System.Globalization.CultureInfo) : string
val processData: data: string -> AsyncWriter<string,string>
val data: string
val result: string
System.String.Replace(oldValue: string, newValue: string) : string
System.String.Replace(oldChar: char, newChar: char) : string
System.String.Replace(oldValue: string, newValue: string, comparisonType: System.StringComparison) : string
System.String.Replace(oldValue: string, newValue: string, ignoreCase: bool, culture: System.Globalization.CultureInfo) : string
val pipeline: input: string -> AsyncWriter<string,string>
Multiple items
module AsyncWriter from AsyncWriterResult

--------------------
type AsyncWriter<'log,'item> = Async<Writer<'log,'item>>
val bind: f: ('a -> AsyncWriter<'log,'b>) -> x: AsyncWriter<'log,'a> -> AsyncWriter<'log,'b>
val finalResult: string
val pipelineLogs: string list

Type something to start searching.