SkillAgentSearch skills...

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/SharpResults

README

SharpResults

<div align="center">

SharpResults Logo NuGet License: MIT Downloads

</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

View on GitHub
GitHub Stars6
CategoryDevelopment
Updated4mo ago
Forks3

Languages

C#

Security Score

72/100

Audited on Nov 18, 2025

No findings