#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}"
|
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}"
|
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}"
|
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")
|
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")
|
Multiple items
module AsyncWriterResult from AsyncWriterResult
--------------------
namespace AsyncWriterResult
--------------------
type AsyncWriterResult<'ok,'error,'log> = Async<Writer<'log,Result<'ok,'error>>>
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
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 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)
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>
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>
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>
<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
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
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 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 ...
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
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
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>>
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
AsyncWriterResult