SharpResults
A lightweight, zero-dependency C# library that implements the Result and Option types for more explicit and type-safe error handling. SharpResults helps you avoid exceptions for control flow and makes success/failure and presence/absence states explicit in your code.
Install / Use
/learn @safwa1/SharpResultsREADME
SharpResults
<div align="center"> </div>A lightweight C# library for functional-style error handling in .NET. No dependencies, just clean error handling with Result<T, TError> and Option<T> types.
Why SharpResults?
Tired of try-catch blocks everywhere? Want to make errors explicit and impossible to ignore? SharpResults helps you write safer code by representing success/failure and presence/absence right in your type signatures. No more hidden exceptions or null reference errors sneaking up on you.
What's in the box
- Type-safe error handling - your errors are part of the type system
- Chainable operations with
Map,AndThen,Match, and friends - Works great with LINQ queries
- Full async/await support
- Pattern matching and deconstruction
- Implicit conversions for cleaner code
- JSON serialization out of the box
- NumericOption for mathematical operations
- Unit type for void-like operations
- Zero dependencies
- Built for .NET 8+
Installation
Install-Package SharpResults
or
dotnet add package SharpResults
Quick Start
Result - when things might fail
Instead of throwing exceptions, return a Result<T, TError>. It's either Ok with a value, or Err with an error.
using SharpResults;
public Result<int, string> ParseInteger(string input)
{
if (int.TryParse(input, out int value))
return value; // Implicit conversion to Ok
return "Not a valid integer"; // Implicit conversion to Err
}
// Use it like this
var result = ParseInteger("123");
result.Match(
ok: value => Console.WriteLine($"Success: {value}"),
err: error => Console.WriteLine($"Error: {error}")
);
// Or with pattern matching
var message = result switch
{
(true, var value, _) => $"Success: {value}",
(false, _, var error) => $"Error: {error}"
};
// Or check the state directly
if (result.IsOk)
{
var value = result.Unwrap();
Console.WriteLine($"Got: {value}");
}
Creating Results
Several ways to create results:
// Explicit factory methods
var ok = Result.Ok<int, string>(42);
var err = Result.Err<int, string>("Something went wrong");
// Implicit conversions (cleaner!)
Result<int, string> result1 = 42; // Converts to Ok
Result<int, string> result2 = "Error"; // Converts to Err
// Try pattern - catches exceptions
var result3 = Result.Try(() => int.Parse("123"));
var result4 = await Result.TryAsync(async () => await GetDataAsync());
// From other types
var fromOption = Result.From(someOption);
var fromFunc = Result.From(() => DoSomething());
Working with Results
// Extract values safely
var value = result.UnwrapOr(0); // Returns value or default
var value2 = result.UnwrapOrElse(err => err.Length); // Compute default from error
var value3 = result.Expect("Expected a number"); // Throws with custom message
// Check state
if (result.WhenOk(out var val))
Console.WriteLine($"Got {val}");
if (result.WhenErr(out var error))
Console.WriteLine($"Error: {error}");
// Deconstruction
var (value, error) = result; // One will be null
Chaining operations
Operations chain together smoothly. If any step fails, the error propagates automatically:
public Result<double, string> GetDiscountedPrice(string userId, string productId)
{
return GetUser(userId)
.AndThen(user => GetProduct(productId).Map(product => (user, product)))
.AndThen(data => CalculateDiscount(data.user, data.product))
.Map(discount => ApplyDiscount(productPrice, discount));
}
// Or with LINQ syntax
var result = from user in GetUser(userId)
from product in GetProduct(productId)
from discount in CalculateDiscount(user, product)
select ApplyDiscount(product.Price, discount);
Option - when values might not exist
Use Option<T> instead of null. It's either Some(value) or None.
public Option<User> FindUserById(string id)
{
var user = _users.FirstOrDefault(u => u.Id == id);
return Option.Create(user); // Some(user) if found, None otherwise
}
// Or use implicit conversion
Option<int> opt = 42; // Becomes Some(42)
// Usage
var option = FindUserById("user-123");
option.Match(
some: user => Console.WriteLine($"Found user: {user.Name}"),
none: () => Console.WriteLine("User not found.")
);
// Provide defaults easily
var userName = option.Map(user => user.Name).UnwrapOr("Guest");
// Pattern matching
if (option.WhenSome(out var user))
Console.WriteLine($"Found: {user.Name}");
// Deconstruction (NET8+)
if (option is (true, var value))
Console.WriteLine($"Got: {value}");
// Filter and chain
var adminOption = FindUserById("123")
.Filter(u => u.IsAdmin)
.Map(u => u.Name);
NumericOption - Options with math
For numeric types, use NumericOption<T> to perform mathematical operations:
NumericOption<int> a = 5;
NumericOption<int> b = 10;
var sum = a + b; // Some(15)
var product = a * b; // Some(50)
NumericOption<int> none = NumericOption<int>.None;
var invalid = a + none; // None - operations with None produce None
// Numeric checks
bool isPositive = NumericOption.IsPositive(a); // true
bool isEven = NumericOption.IsEvenInteger(b); // true
// Parse numbers safely
var parsed = NumericOption<int>.Parse("123"); // Some(123)
var failed = NumericOption<int>.Parse("abc"); // None
// Convert between types
Option<int> regularOption = myNumericOption; // Implicit conversion
Unit type - for void-like operations
When you want to return Result from a void method:
public Result<Unit, string> SaveData(string data)
{
try
{
File.WriteAllText("data.txt", data);
return Unit.Default; // Success with no value
}
catch (Exception ex)
{
return ex.Message; // Error
}
}
// Or use Result.From for actions
var result = Result.From(() => File.Delete("temp.txt"));
Async support
Everything works seamlessly with async/await:
public async Task<Result<string, string>> GetUserDataAsync(string url)
{
return await Result.TryAsync(async () =>
{
using var client = new HttpClient();
return await client.GetStringAsync(url);
})
.MapErr(ex => ex.Message);
}
// Async transformations for Result
var result = await GetUserAsync(id)
.MapAsync(async user => await GetProfileAsync(user))
.AndThenAsync(async profile => await EnrichAsync(profile))
.MapOrElseAsync(
mapper: async data => await FormatAsync(data),
defaultFactory: async err => await GetDefaultAsync()
);
// Async transformations for Option
var option = await FindUserAsync(id)
.MapAsync(async user => await user.GetNameAsync())
.AndThenAsync(async name => await ValidateAsync(name))
.OrElseAsync(async () => await GetDefaultNameAsync());
// Async with bool extensions
var result = await isValid.ThenAsync(async () => await LoadDataAsync());
var option = await hasPermission.ThenSomeAsync(GetUserDataAsync());
// Work with async sequences
await foreach (var value in asyncOptions.ValuesAsync())
{
Console.WriteLine(value);
}
var firstAsync = await asyncSequence.FirstOrNoneAsync();
var filteredAsync = await asyncSequence.FirstOrNoneAsync(x => x > 10);
// Async collection operations for Results
await foreach (var success in asyncResults.ValuesAsync())
{
ProcessSuccess(success);
}
await foreach (var error in asyncResults.ErrorsAsync())
{
LogError(error);
}
LINQ Integration
Use results and options in LINQ queries:
// Query syntax
var result = from user in GetUser(id)
from orders in GetOrders(user.Id)
from total in CalculateTotal(orders)
select total;
// Method syntax
var names = users
.Select(u => FindUserName(u.Id))
.WhereSome() // Filter out None values
.ToList();
// Working with collections
var results = ids
.Select(id => GetUser(id))
.Collect(); // Result<List<User>, TError>
Collection helpers
Work with collections safely:
// Safe LINQ operations
var first = users.FirstOrNone(); // Option<User>
var last = users.LastOrNone(u => u.IsActive);
var single = users.SingleOrNone(u => u.Id == id);
var element = users.ElementAtOrNone(5);
// Dictionary operations
var value = dict.GetValueOrNone(key); // Option<TValue>
// Stack and Queue operations
var peeked = stack.PeekOrNone(); // Doesn't remove
var popped = stack.PopOrNone(); // Removes and returns
var dequeued = queue.DequeueOrNone();
// PriorityQueue
var next = priorityQueue.PeekOrNone<Task, int>(); // (Task, Priority)
var task = priorityQueue.DequeueOrNone<Task, int>();
// Concurrent collections (thread-safe)
var item = concurrentBag.TakeOrNone();
var value = concurrentStack.PopOrNone();
// Sets
var found = hashSet.GetValueOrNone(searchValue);
var item = sortedSet.GetValueOrNone(value);
// SelectWhere - transform and filter in one pass
var results = items
.SelectWhere(x => x > 0 ? Option.Some(x * 2) : Option.None<int>());
// Extract all Some values from a sequence
var values = options.Values(); // IEnumerable<T>
// Sequence - convert list of Options to Option of list
var allOrNone = options.Sequence(); // Option<IEnumerable<T>>
var listOrNone = options.SequenceList(); // Option<List<T>>
Bool extensions
Use booleans to create Options:
// Execute function conditionally
var result = isValid.Then(() => ProcessData()); // Option<T>
// Create Some/None based on condition
var option = hasPermission.ThenS
Related Skills
node-connect
345.9kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
106.4kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
345.9kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
345.9kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
