Skip to content

F# Guide — Task: .NET Task-Based Asynchronous Programming

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# task computation expressions (available in F# 6+) provide direct support for .NET System.Threading.Tasks.Task, enabling seamless interop with C# async code and libraries using a familiar async/await-style syntax.

What You'll Learn

  • Task computation expressions
  • Task vs Async comparison
  • Converting between Task and Async
  • Error handling in tasks
  • Cancellation support

Why It Matters

Tasks are the standard .NET async model. F# task CE enables direct interop with C# libraries, ASP.NET Core, and the broader .NET ecosystem. Durga Antivirus Pro uses tasks for its .NET integration layer.

Real-World Use

ASP.NET Core controllers, Entity Framework queries, Azure SDK calls, and any .NET library using Task-based async.

flowchart LR
    A["Task"] --> B["task { }"]
    B --> C["let! binding"]
    C --> D["Interop"]
    D --> E["Best Practices"]
    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

Task Computation Expression

// Basic task
let fetchDataAsync url = task {
    let! html = httpClient.GetStringAsync(url)
    return html.Length
}

// Multiple steps
let processDataAsync url = task {
    let! data = fetchDataAsync url
    let! saved = saveToDatabaseAsync data
    return saved
}

Task vs Async

// Async (F# native)
let asyncWork = async {
    let! result = someAsyncWork
    return result * 2
}

// Task (F# 6+)
let taskWork = task {
    let! result = someTaskWork
    return result * 2
}

// Differences:
// - Tasks start immediately on creation
// - Async computations are lazy, start on demand
// - Tasks integrate with C# async/await
// - Tasks can be awaited with let! inside task CE

Interop with C#

// Calling C# async methods
let callCsharpLibraryAsync = task {
    let client = new System.Net.Http.HttpClient()
    let! response = client.GetStringAsync("http://example.com")
    return response.Length
}

// Exposing tasks to C#
let methodReturningTask x = task {
    do! Task.Delay 100
    return x * 2
}
// C# sees: Task<int> methodReturningTask(int x)

Error Handling

let safeTask = task {
    try
        let! result = riskyOperationAsync()
        return Ok result
    with
    | :? System.Net.Http.HttpRequestException as ex ->
        return Error (sprintf "HTTP error: %s" ex.Message)
    | ex ->
        return Error (sprintf "Error: %s" ex.Message)
}

// Task with cancellation
let cancellableTask ct = task {
    for i in 1..100 do
        ct.ThrowIfCancellationRequested()
        do! Task.Delay(100, ct)
    return 42
}

Converting Between Task and Async

// Async to Task
let asyncComp = async { return 42 }
let taskFromAsync: Task<int> = asyncComp |> Async.StartAsTask

// Task to Async
let taskComp = Task.Run(fun () -> 42)
let asyncFromTask: Async<int> = taskComp |> Async.AwaitTask

// task CE can await both
let mixed = task {
    let! asyncResult = asyncComp |> Async.StartAsTask
    let! taskResult = taskComp
    return asyncResult + taskResult
}

Parallel Tasks

let parallelTasks = task {
    let task1 = fetchDataAsync "http://a.com"
    let task2 = fetchDataAsync "http://b.com"

    // Tasks start immediately (not lazy)
    let! result1 = task1
    let! result2 = task2

    return result1 + result2
}

// Better parallel with WhenAll
let parallelWhenAll = task {
    let urls = ["http://a.com"; "http://b.com"; "http://c.com"]
    let! results =
        urls
        |> List.map fetchDataAsync
        |> Task.WhenAll
    return results |> Array.sum
}

Resource Management

let readFileAsync path = task {
    use stream = new System.IO.FileStream(path, System.IO.FileMode.Open)
    use reader = new System.IO.StreamReader(stream)
    let! content = reader.ReadToEndAsync()
    return content
}

// use! for async disposal
let processResourceAsync = task {
    use! resource = acquireResourceAsync()
    let! result = resource.DoWorkAsync()
    return result
}

Common Mistakes

1. Blocking on tasks

Calling .Result or .Wait() blocks threads. Always use let! inside task CE.

2. Tasks as lazy values

Unlike Async, tasks start executing immediately upon creation. Create them just before awaiting.

3. Forgetting cancellation tokens

Long-running tasks should accept and respect cancellation tokens for responsiveness.

4. Mixing task and async without conversion

Don't use let! on an F# async inside task CE. Convert with Async.StartAsTask first.

5. Task.Run overhead

Use task { } for async I/O work. Use Task.Run only for CPU-bound background work.

Practice Questions

1. How is task different from async? Tasks start immediately upon creation. Async computations are lazy. Tasks integrate directly with .NET Task-based async.

2. How do you call C# async methods in F#? Use task { let! result = csharpMethodAsync() ... }. The task CE directly awaits .NET tasks.

3. What happens if a task throws? The exception is captured in the task. When awaited with let!, it's rethrown. Use try-with inside task CE to handle it.

Challenge: Write a task-based function that downloads two URLs in parallel and returns the combined content length.

FAQ

{{< faq question="Should I use task or async?" >}} Use task for .NET interop, ASP.NET Core, and when C# code will consume your async methods. Use async for F#-only code and when you need lazy computations. {{< /faq >}}

{{< faq question="Are tasks hot or cold?" >} Tasks are hot (start immediately). Async computations are cold (start on demand). This is a fundamental difference. {{< /faq >}}

{{< faq question="Can I cancel a task?" >} Yes. Pass a CancellationToken to your task and check ct.ThrowIfCancellationRequested() periodically. {{< /faq >}}

{{< faq question="What is Task.WhenAll?" >} A .NET method that creates a task completing when all provided tasks complete. Useful for parallel execution. {{< /faq >}}

{{< faq question="Can I use async disposables with tasks?" >} Yes. use! in task CE disposes resources asynchronously when they implement IAsyncDisposable. {{< /faq >}}

Mini Project

Build an async web API client using task CE:

type JsonPlaceholderClient() =
    let http = new System.Net.Http.HttpClient()

    member this.GetUserAsync userId = task {
        let! response = http.GetStringAsync($"https://jsonplaceholder.typicode.com/users/{userId}")
        return response
    }

    member this.GetUsersAsync() = task {
        let! response = http.GetStringAsync("https://jsonplaceholder.typicode.com/users")
        return response
    }

    member this.GetAllAsync userIds = task {
        let tasks = userIds |> List.map this.GetUserAsync
        let! results = Task.WhenAll(tasks)
        return results |> Array.toList
    }

What's Next

Now that you understand tasks, explore MailboxProcessor for agent-based concurrency.

Topic Description Link
F# MailboxProcessor Agent-based concurrency {{< ref "20-mailbox-processor" >}}
F# Queries Query expressions {{< ref "21-queries" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro