FsToolkit.ErrorHandling: railway-style error handling for F# Result types
An opinionated F# Library for error handling
At a glance
- What is it?
- FsToolkit.ErrorHandling is an MIT-licensed F# utility library for the Result type, providing map, bind, traverse and sequence, computation expressions and infix operators across Result, Result of option, Async of Result and list of Result, with NuGet packages for Task, Job, AsyncSeq and IcedTasks. It suits an F# team working in railway-oriented style, and it publishes five beta releases of a 6.0 line.
- Who is it for?
- FsToolkit.ErrorHandling is worth adopting if your F# codebase already leans on Result and you want the plumbing to stop being hand-written, because the library supplies the map, bind, apply, traverse and sequence that every pipeline needs along with computation expressions that make an error path read as a sequence of steps, and because the same vocabulary is available for Async, Task, Job, AsyncSeq and IcedTasks, so an asynchronous codebase does not have to reimplement the pattern.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 11 days ago.
- What is it written in?
- Mainly F#, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the library is, and the type surface it covers
FsToolkit.ErrorHandling is a utility library to work with the Result type in F#, and it allows you to do clear, simple and powerful error handling, as the project puts it.
The functions it provides are the ones every functional pipeline needs: map, bind, apply, traverse and sequence, along with computation expressions and infix operators. What distinguishes it from a handful of functions in your own Utilities module is the range of types it works over.
The list is specific. It handles Result<'a, 'b>, Result<'a option, 'b>, Async<Result<'a, 'b>>, Async<Result<'a option, 'b>>, and Result<'a, 'b list>. That last one is the one worth pausing on, because a list of Results is the shape of a batch operation that can fail in several places, and treating it as a single value with a single error type is a different modelling decision from a list of Results where each element carries its own error.
The Async and Async of Result variants cover the asynchronous case, and the option variant covers the case where the success value is itself optional. So the library is not one abstraction stretched over several types, it is a set of related abstractions handled consistently.
The project also explains where the idea came from. It was inspired by Chessie and by Cvdm.ErrorHandling, and the latter has now been merged into FsToolkit.ErrorHandling. So this is a consolidation of two prior libraries into one, which is a useful thing to know if you have code against either of them.
The targets are .NET Standard 2.0 and .NET Standard 2.1, and Fable is supported. The primary language is F#, the licence is MIT, and the recent releases are 6.0.0-beta003 from 2026-10-12, 6.0.0-beta002 from 2026-09-16 and 6.0.0-beta001 from 2026-06-02, with the last push to the repository on 2026-10-12.
The login example, and what a computation expression buys you
The motivating example in the project documentation is a login flow, and it is worth reading closely because it is the clearest statement of what the library is for.
// Given the following functions:
// tryGetUser: string -> Async<User option>
// isPwdValid: string -> User -> bool
// authorize: User -> Async<Result<unit, AuthError>>
// createAuthToken: User -> Result<AuthToken, TokenError>
type LoginError = InvalidUser | InvalidPwd | Unauthorized of AuthError | TokenErr of TokenError
let login (username: string) (password: string) : Async<Result<AuthToken, LoginError>> =
asyncResult {
// Async.req parses a value using a Req.* function, or yields the specified error if None
let! user = username |> tryGetUser |> Async.req Req.some InvalidUser
// Req.filter gives the specified error if isPwdValid returns false
do! user |> Req.filter (isPwdValid password) InvalidPwd
// Error value is wrapped/transformed (Unauthorized has signature AuthError -> LoginError)
do! user |> authorize |> AsyncResult.mapError Unauthorized
// Same as above, but synchronous, so we use the built-in mapError
return! user |> createAuthToken |> Result.mapError TokenErr
}Four things happen in that block. A user is fetched, and if the lookup returns None the flow yields InvalidUser. The password is checked, and a failure yields InvalidPwd. The user is authorized, and an AuthError is wrapped into the wider LoginError with the mapError function. And a token is created synchronously, with the same error transformation using the built-in mapError.
The result is that the error type of the whole function is declared in one place, as a discriminated union, and every step of the flow names the error it can produce. A reader can see the failure modes of the function without reading the bodies of the four helper functions.
That is the railway idea: a computation expression turns a sequence of fallible steps into a single track, and the error is carried along the track rather than thrown.
The library also uses the Req helpers as a separate idea from the computation expression itself. Async.req parses a value using a Req.* function, or yields the specified error if None, and Req.filter gives the specified error if a predicate returns false. Those are functions you can use outside a computation expression, which is what makes them composable with the ordinary F# library.
Five NuGet packages, one library family
The library is published as a family of packages rather than a single one, and the shape of the family tells you what problem it is solving.
The main package is FsToolkit.ErrorHandling. Alongside it there are FsToolkit.ErrorHandling.TaskResult, FsToolkit.ErrorHandling.JobResult, FsToolkit.ErrorHandling.AsyncSeq and FsToolkit.ErrorHandling.IcedTasks.
Each of those is the same pattern applied to a different async model. The F# standard library has Task and Job, and the wider ecosystem has AsyncSeq for asynchronous sequences and IcedTasks for a different task scheduler. A library that only handled Async would force you to convert when a dependency returned a Task, and the conversion is where error handling tends to get lost.
The package table in the documentation lists each one twice, once for the stable release and once for the pre-release, with a link to the pre-release listing for each. That is a small detail with a practical consequence: the 6.0 line is in beta, and the stable packages are a different, older line. A team has to decide which line it is on, because the APIs can differ between them.
The documentation itself is a GitBook, hosted at demystifyfp.gitbook.io, and the repository carries the sources for it in a gitbook/ directory with a book.json, alongside a playground.fsx, which is a F# script you can execute to try things without building anything.
The project also links the main resource for learning this style of programming, the Railway Oriented Programming material by Scott Wlaschin, and then says something that deserves attention: however Result isn't a panacea, and it links a talk with the title Don't Throw Exceptions, Use A Result Pattern is BAD ADVICE!
That is a library author telling you that his own tool is not always the answer, and linking a talk that argues against the general advice. For an engineer choosing between exceptions and Result in F#, that is a more useful pointer than another library that only sells its own approach.
The build, the devcontainer and testing across three runtimes
The build system is a script, and the documentation lists the targets rather than assuming you know them.
Compiling is a build.cmd with an optional build target on Windows, and a ./build.sh with an optional build target on unix. Without specifying a target, the default is DotnetPack, which runs the tests for all projects on dotnet and then packs the projects into NuGet packages. So a plain build is a test run followed by a package, which is a sensible default for a library.
The named targets are Clean, which cleans the bin and obj folders of all projects; DotnetTest, which runs the tests for the dotnet projects; NpmTest for the fable-javascript projects; PythonTest for the fable-python projects; RunTests, which runs all three; and FormatCode, which runs fantomas to format the codebase. The documentation is explicit that this is not an exhaustive list and that more targets are in the ./build/build.fs file, so the build is scripted in F# itself, which is a common pattern in the F# ecosystem.
The local setup needs the .NET Core SDK, and the documentation lists versions 6.x, 7.x and 8.x. The optional part, needed only to test the Fable builds locally, adds Node at v18.0.0 or higher with NVM recommended for managing multiple versions, and Python at v3.10.0 or higher, which is required for Fable-Python.
That Fable support is the reason the test matrix is three wide. The package.json at the root of the repository shows the Fable side of it: the pretest script for a project runs dotnet fable with an output directory, and the test script then runs the generated JavaScript with node and a timeout of 60000. There are matching watch-test scripts that run dotnet fable watch with a runWatch argument, and npm-run-all orchestrates them. So the same test suite is compiled to JavaScript and executed in Node, which is a genuine cross-runtime check rather than a claim.
For contributors there is a devcontainer setup for VSCode, and the repository tree shows the rest of the layout: src/, tests/, benchmarks/ with a BenchmarkDotNet.Artifacts directory, tools/, a Directory.Build.props and a Directory.Packages.props for central package management, a global.json pinning the SDK, an .nvmrc, an AGENTS.md and a RELEASE_NOTES.md.
Where FsToolkit.ErrorHandling is the wrong tool
The first limit is the beta line. The recent releases are 6.0.0-beta001, 6.0.0-beta002 and 6.0.0-beta003, and a beta means the API can still move between previews. For a library that other code depends on, that is a version to pin with care rather than float on, and the fact that the NuGet listing carries both a stable line and this pre-release line makes the choice explicit.
The second is that Result is a modelling decision, not a free improvement, and the project says so. The linked material on where Result should not be used is the counterweight to the enthusiasm, and the honest reading is that turning every fallible operation into a Result in a codebase that is not written for it produces noisy pipelines that are harder to read than the exceptions they replaced.
The third is the F# specificity. The library is for F#, and its audience is a team that has already chosen Result as its error representation. A team that throws exceptions for expected failures, or that uses option for absence and exceptions for failure, will not get value from it, and adopting it for one module is worse than not adopting it at all.
The fourth is the Fable angle, which is both a strength and a constraint. Being able to compile to JavaScript and to Python means the same error handling code can run on the server and in a browser, and the Fable test matrix in the build is the reason that claim is credible. It also means the library has to be careful about which BCL types it uses, and the .NET Standard 2.0 target is partly that constraint.
And the fifth is the dependency count of the family. Five packages means five things to track, and the Task, Job, AsyncSeq and IcedTasks variants exist because the async model in a given codebase is often decided by a framework you did not choose. A team that standardises on one of them can install one package, and a team that does not is carrying four.
Editorial conclusion
FsToolkit.ErrorHandling is worth adopting if your F# codebase already leans on Result and you want the plumbing to stop being hand-written, because the library supplies the map, bind, apply, traverse and sequence that every pipeline needs along with computation expressions that make an error path read as a sequence of steps, and because the same vocabulary is available for Async, Task, Job, AsyncSeq and IcedTasks, so an asynchronous codebase does not have to reimplement the pattern. It is the wrong tool if you do not use Result, and it is the wrong tool if you want the library to tell you where Result is a bad idea. Before you adopt it, check three things: that the version you take is the one you intend, because the recent releases are 6.0.0-beta001, beta002 and beta003 between June and October 2026 and a beta line means the API can still move; that the target framework suits you, since the library targets .NET Standard 2.0 and .NET Standard 2.1 and supports Fable, which means it runs on .NET Framework, on .NET and on the Fable compiler targets for JavaScript and Python; and that you read the linked material on where Result is the wrong tool, because the project documentation points at a talk arguing that do not throw exceptions, use a Result pattern is bad advice, and a library that is that opinionated deserves a second opinion.
Frequently asked questions
Which types does FsToolkit.ErrorHandling work with?
The library provides map, bind, apply, traverse and sequence, plus computation expressions and infix operators, for Result<'a, 'b>, Result<'a option, 'b>, Async<Result<'a, 'b>>, Async<Result<'a option, 'b>> and Result<'a, 'b list>. It targets .NET Standard 2.0 and .NET Standard 2.1 and supports Fable.
Which NuGet packages does the FsToolkit.ErrorHandling family publish?
There are five: FsToolkit.ErrorHandling, FsToolkit.ErrorHandling.TaskResult, FsToolkit.ErrorHandling.JobResult, FsToolkit.ErrorHandling.AsyncSeq and FsToolkit.ErrorHandling.IcedTasks. Each is the same Result pattern applied to a different asynchronous model, and each is published on NuGet with both a stable and a pre-release listing.
How do I build and test FsToolkit.ErrorHandling locally?
Run ./build.sh on unix or build.cmd on Windows, optionally with a build target. Without a target the default is DotnetPack, which runs the tests for all projects and then packs them into NuGet packages. Named targets include Clean, DotnetTest, NpmTest, PythonTest, RunTests and FormatCode, and more are in the ./build/build.fs file. A devcontainer setup is provided for VSCode.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/demystifyfp-fstoolkit-errorhandling)Was this useful?