A minimal Result / Result<TError> for library authors who want to publish a closed, exhaustively-checkable contract of failure modes - with a separate channel for native/platform exceptions. No combinators, no extensibility hooks - the library defines its failure surface, the consumer reacts.
Most Result libraries (FluentResults, ErrorOr, Ardalis.Result) are designed for application-level error pipelines: open polymorphic errors, combinators (Map / Bind / Match), reasons / metadata chains. That's the wrong shape for a library boundary type, where the calls:
- have a fixed, documented set of failure modes (good fit for an enum),
- can also throw raw platform/SDK exceptions that the library can't fully classify,
- are translated by the consumer at the boundary, not pipelined deeper.
This package occupies that niche.
dotnet add package Kebechet.Types.Result| Type | When to use |
|---|---|
Result | Library call that returns no value; only failure is "it threw." |
Result<TError> | Library call that returns no value; closed enum of documented failures + native exception channel. |
DataResult<TValue> | Library call that returns a value; only failure is "it threw." |
DataResult<TValue, TError> | Library call that returns a value; closed enum of documented failures + native exception channel. |
The non-generic Result types and the DataResult types can also be subclassed to give per-operation return types named docs (e.g. WriteHealthDataResult : Result<WriteError> carrying its own RecordIds property). Use the generic DataResult directly for simple cases; subclass when you want a self-documenting return type.
When the call has only one failure shape - "it threw":
usingTypes.Result;publicclassConnectResult:Result{publicstring?SessionId{get;init;}}publicConnectResultConnect(){try{varsession=_native.OpenSession();returnnewConnectResult{SessionId=session.Id};}catch(Exceptionex){returnnewConnectResult{ErrorException=ex};}}When the library has a closed set of documented failure modes and also needs to surface raw platform exceptions:
usingTypes.Result;publicenumWriteError{PermissionDenied,SdkUnavailable,QuotaExceeded}publicclassWriteResult:Result<WriteError>{publicIReadOnlyList<string>RecordIds{get;init;}=[];}publicWriteResultWrite(IList<Record>items){if(!_hasPermission){returnnewWriteResult{Error=WriteError.PermissionDenied};}try{varids=_native.Insert(items);returnnewWriteResult{RecordIds=ids};}catch(Exceptionex){returnnewWriteResult{ErrorException=ex};}}Consumer side:
varresult=library.Write(items);if(result.IsSuccess){Persist(result.RecordIds);return;}if(result.Erroris{}error){varmessage=errorswitch{WriteError.PermissionDenied=>"Grant permission and retry.",WriteError.SdkUnavailable=>"SDK not installed.",WriteError.QuotaExceeded=>"Try again later."};ShowError(message);return;}LogPlatformException(result.ErrorException!);The switch expression over a WriteError enum is exhaustively checked by the compiler (warning CS8509 - promote to error in your csproj for hard enforcement).
For simple cases where a per-operation type would be overkill:
usingTypes.Result;publicDataResult<int>CountSessions(){try{returnnewDataResult<int>{Value=_native.CountSessions()};}catch(Exceptionex){returnnewDataResult<int>{ErrorException=ex};}}publicDataResult<UserProfile,ProfileError>GetProfile(stringid){if(!_hasPermission){returnnewDataResult<UserProfile,ProfileError>{Error=ProfileError.Forbidden};}try{varprofile=_native.LoadProfile(id);returnnewDataResult<UserProfile,ProfileError>{Value=profile};}catch(Exceptionex){returnnewDataResult<UserProfile,ProfileError>{ErrorException=ex};}}Reading the value:
varresult=service.GetProfile("abc");if(!result.IsSuccess){HandleFailure(result);return;}varprofile=result.Value!;// safe to dereference once IsSuccess is checkedValue is only meaningful when IsSuccess is true. On failure, reference-typed values are null and value-typed values are default(TValue) - always check IsSuccess first.
| This package | FluentResults / ErrorOr | |
|---|---|---|
| Errors are a closed set | yes - enum | no - open polymorphic objects |
| Compiler-checked exhaustive matching | yes | no |
| Per-error data fields | no - enum is just an int | yes - subclass Error |
| Native exception channel | yes - first-class peer | no - collapsed into a generic error |
Combinators (Map, Bind, Match) | no | yes |
| Value carrier | per-operation subclass | Result<T> |
| Dependencies | none | varies |
If you need rich per-error metadata, error chaining, or pipeline composition, reach for FluentResults or ErrorOr. If you're publishing a library that wants a closed failure contract and a place to put raw platform exceptions, this is the smaller, sharper tool.
