Action
C#
programming
methods
return value

How to return value from Action?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In C#, Action is a delegate type that represents a method returning void — it performs work but does not return a value. If you need a delegate that returns a value, use Func<TResult> instead. However, there are legitimate scenarios where you have an existing Action-based API and need to extract a result. The workarounds include using Func<T> (the correct solution), out parameters, captured variables (closures), or wrapping the Action in a Task.

Action vs Func — The Core Difference

csharp
1// Action — returns void
2Action greet = () => Console.WriteLine("Hello");
3Action<string> greetName = (name) => Console.WriteLine($"Hello, {name}");
4Action<int, int> add = (a, b) => Console.WriteLine(a + b);
5
6// Func — returns a value (last type parameter is the return type)
7Func<int> getNumber = () => 42;
8Func<string, int> getLength = (s) => s.Length;
9Func<int, int, int> multiply = (a, b) => a * b;
10
11int result = multiply(3, 4);  // 12

If you need a return value, Func<TResult> is the correct delegate type.

csharp
1// WRONG — trying to get a return value from Action
2Action calculate = () => { /* can't return anything */ };
3
4// CORRECT — use Func<T>
5Func<int> calculate = () => 42;
6int result = calculate();  // 42
7
8// With parameters
9Func<string, string, string> concatenate = (a, b) => $"{a} {b}";
10string fullName = concatenate("John", "Doe");  // "John Doe"

If you control the API, change Action parameters to Func<T>:

csharp
1// Before — no way to get result
2public void Execute(Action operation)
3{
4    operation();
5}
6
7// After — can return result
8public T Execute<T>(Func<T> operation)
9{
10    return operation();
11}
12
13// Usage
14int result = Execute(() => 42);

Solution 2: Captured Variable (Closure)

When you cannot change the delegate type (e.g., third-party API), capture a variable in the closure.

csharp
1int result = 0;
2
3Action compute = () =>
4{
5    result = ExpensiveCalculation();  // Write to captured variable
6};
7
8compute();
9Console.WriteLine(result);  // Contains the computed value
csharp
1// Real-world example: extracting data from an Action-based callback API
2string responseBody = null;
3
4Action<HttpClient> fetchData = (client) =>
5{
6    var response = client.GetStringAsync("https://api.example.com/data").Result;
7    responseBody = response;  // Captured variable
8};
9
10fetchData(new HttpClient());
11Console.WriteLine(responseBody);  // Contains the response

Solution 3: out Parameter via Wrapper

csharp
1public delegate void ActionWithResult<T>(out T result);
2
3public static T ExecuteWithResult<T>(ActionWithResult<T> action)
4{
5    action(out T result);
6    return result;
7}
8
9// Usage
10int value = ExecuteWithResult((out int result) =>
11{
12    result = 42;
13});
14Console.WriteLine(value);  // 42

Solution 4: Convert Action to Func with a Wrapper

csharp
1public static Func<T> ToFunc<T>(Action action, T defaultResult)
2{
3    return () =>
4    {
5        action();
6        return defaultResult;
7    };
8}
9
10// When you need to pass an Action where Func is expected
11Action sideEffect = () => Console.WriteLine("Done");
12Func<bool> wrapped = ToFunc(sideEffect, true);
13bool result = wrapped();  // Prints "Done", returns true

Solution 5: Using Task for Async Scenarios

csharp
1// Action-based async pattern
2public async Task<int> ExecuteAsync(Action<Action<int>> operation)
3{
4    var tcs = new TaskCompletionSource<int>();
5    operation(result => tcs.SetResult(result));
6    return await tcs.Task;
7}
8
9// Usage
10int result = await ExecuteAsync(callback =>
11{
12    // Simulate async work
13    int computed = 42;
14    callback(computed);  // Signal completion with result
15});

When Each Approach Is Appropriate

ScenarioApproach
You control the APIChange Action to Func<T>
Third-party callback APICaptured variable (closure)
Need thread-safe result passingTaskCompletionSource<T>
Simple one-off extractionout parameter wrapper
Adapting Action for Func-expecting APIWrapper function

Common Pitfalls

  • Using Action when Func is what you need: If you are designing an API and the operation produces a result, use Func<TResult> from the start. Trying to extract return values from Action is a workaround, not a pattern.
  • Thread safety with captured variables: When using closures to capture results across threads, the captured variable is not thread-safe. The Action may complete on a different thread, and reading the variable without synchronization causes race conditions. Use TaskCompletionSource<T> or Interlocked for cross-thread result passing.
  • Calling .Result on tasks inside Action: Using .Result or .Wait() on async operations inside an Action can deadlock, especially in UI or ASP.NET contexts with a synchronization context. Use async/await with Func<Task<T>> instead.
  • Confusing Action<T> with Func<T>: Action<T> takes a parameter of type T and returns void. Func<T> takes no parameters and returns T. Func<T, TResult> takes T and returns TResult. Mixing these up causes compile errors that may be confusing.
  • Over-engineering with wrappers when refactoring is possible: If you can change the delegate type, just change it. Adding wrapper classes, captured variables, and adapter patterns around Action to simulate return values adds unnecessary complexity. Refactor to Func<T> when feasible.

Summary

  • Action returns void — use Func<TResult> when you need a return value
  • If you cannot change the delegate type, use a captured variable (closure) to extract the result
  • Use TaskCompletionSource<T> for async scenarios with callback-based APIs
  • Func<T> is the idiomatic C# approach — prefer it over workarounds
  • Avoid .Result and .Wait() inside Action delegates to prevent deadlocks

Course illustration
Course illustration

All Rights Reserved.