Monio: an IO monad for JavaScript with do-style syntax and promise interop
The most powerful IO monad implementation in JS, possibly in any language!
At a glance
- What is it?
- Monio is a JavaScript library that provides an async-capable IO monad, including a do-style method that takes JS generators, plus companion Maybe, Either, State, and IO and IOx variants. It is lazy, it unwraps promises produced inside a chain, and it chooses promises over a Future monad on purpose, and the project documentation is unusually open about the fact that its headline capability claim is a marketing statement rather than a theorem.
- Who is it for?
- Monio fits a JavaScript developer who wants side effects modelled explicitly with a lazy IO monad, who likes generator based do-notation, and who wants exceptions and Either results to compose with the same chain rather than living in separate error handling paths.
- 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?
- Activity is slowing. The repository last received commits 6 months ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.
Star history
- getify/monio1,124 ★ · -1
| Repository | 2026-10-19 | 2026-11-07 |
|---|---|---|
| getify/monio | 1.1k | 1.1k |
-1 Stars in the last 19 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
The headline claim, and the author's own caveat about it
Monio's opening claim is that its IO and IOx are the most powerful IO monad implementation in JS, possibly in any language. The project notes immediately undercut it, and the honesty is the first thing worth noting about the project. It says this is obviously a marketing claim, not a provable mathematical or scientific assertion, and adds Nevertheless, I believe it's true.
That framing tells you something about how the rest of the documentation is written. A library that models side effects, laziness and error handling in a language whose mainstream style is imperative needs to explain its philosophy before its API, and the project documentation does exactly that. The next section, Wait, What's A Monad?, branches the reader three ways. If you are already comfortable with functional programming and familiar with monads and the code snippet does not scare you, you skip to See Monio In Action. If you are just looking for quick code examples of all the monads, there is a MONIO.md file. And if terms like higher-order function or monad are confusing, the documentation encourages you to build comfort first, arguing that those are the foundations you need to get the most out of Monio.
The author is careful about not talking down to the reader. The author writes that he doesn't want you to feel intimidated, and that it isn't going to require you to speak in mathematical notation or re-learn everything you thought you knew about programming, and then makes the point that you don't have to do all that just to get started. There are small and comfortable baby steps you can begin taking.
The path leads to MONADS.md, which is described as a dive into FP and then monads, with an Expansive Intro To Monads section for readers who already have the FP background. The documentation asks you to take your time with it, because it is long and detailed and there is a lot to digest.
This is worth knowing before you evaluate the code. Monio is not a drop-in utility with a small surface. It is a teaching-oriented library with three substantial Markdown documents alongside the source, and the onboarding cost is part of the product.
Laziness, promise unwrapping, and why IO does nothing until run
The core of the design is that IOs are lazy, so their operations are not triggered until the run method is called. The driving inspiration is the IO monad, useful for managing side effects, which additionally supports do-style syntax with JS-ergonomic asynchrony based on promises, in the style of familiar async and await code.
The promise handling is the part that distinguishes this from a textbook IO monad. Monio's IO comes pre-transformed for promises, which means that when promises are produced in an IO, they are automatically unwrapped, and of course that means subsequent IO operations are deferred. If any IO in a chain produces a promise, the result of run will be lifted to a promise that resolves when the entire IO chain is complete. Otherwise the IO instance and its run call operate synchronously and immediately produce the result.
So a single library covers both the synchronous case and the asynchronous case without you choosing up front, and the return type of run tells you which happened. That is a pragmatic choice rather than a pure one.
The rationale is given directly. Monio intentionally chooses to model asynchrony over promises instead of Future monads, because of its goal of balancing FP with pragmatic and idiomatic non-FP JS. And it adds that there is nothing that should prevent you from using a Future monad with Monio if you prefer, which is a slightly odd thing to say for a library that does not ship a Future, but it makes the point that the choice is about ergonomics rather than about capability.
For the type theory inclined, the documentation gives the signature. IO models a function e => IO a (Promise b c), which is strong enough to capture optional environment passing, side effects, async and error handling without the pain of composing each type separately. Typically IO does not take an argument, but given one it acts like an effectful Reader. It can also model sync or async functions so the inner Promise becomes optional. The shorthand offered is ReaderT (IOT (Promise|Identity a b)), where Promise gets swapped for Identity if you are not doing async.
The Reader bit is the last piece of the mechanism. IO is also pre-transformed with Reader, which carries side-effect read environments alongside IO operations, so environment passing is part of the same monad rather than a layer you stack on top.
do-style generators, yield for IO and await for plain promises
The do-style syntax is specified with the do method, which automatically lifts the IO to promise-producing asynchrony, and it accepts JS generators including async generators, written as async function *whatever(){ .. }. The two keywords have different jobs, and the distinction is the one thing to get right.
Yield is used for chaining IOs, which can produce promises to defer. Await is for explicitly deferring on a promise that is not already wrapped in an IO. The claim is that the resulting style of code should be more broadly approachable for JS developers while still benefiting from monads.
That is a fair description of what generator-based do-notation buys you. Instead of nesting callbacks, you write a linear sequence of effects and yield each one, and the runtime reassembles them. Because the generator can be an async generator, the same shape covers the deferred case.
Exception handling is folded into the same mechanism rather than being a separate error channel. IO's do is JS-ergonomic for exception handling: uncaught JS exceptions become promise rejections, and IO-produced promise rejections are try and catch able. IO also supports modelling exception handling through Either monads, where doEither transforms uncaught exceptions into Either:Left values and recognises IO-produced Either:Left values as try and catch able exceptions.
So there are two parallel error representations, and the library lets you move between them. That is more capability than a typical monad library offers, and it is also more surface to learn, since you have to know which representation an error is in at any point in a chain.
The package layout shows the full set of monads the library ships, which is visible in the package.json export map. The root is Monio. Then just.js exports Just, nothing.js exports Nothing, maybe.js exports Maybe, either.js exports Either, state.js exports State. Under io/ there are io.js for IO, iox.js for IOx, any.js for AnyIO, all.js for AllIO, helpers.js for IOHelpers, and x-helpers.js for IOxHelpers. lib/util.js exports MonioUtil.
The naming is a little confusing at first glance, since io/ is the directory and io.js is one of the files in it, but the set tells you the shape of the library: one core IO, a reactive IOx variant, the choice monads Maybe and Either, a State monad, and two aggregation monads, AnyIO and AllIO, for combining.
The demo surface, and the choice to be explicit about side effects
The See Monio In Action section exists to show what the code looks like, and it is a long list of runnable examples rather than a description.
There is a full demo app, client and server, at the Monio Demo App repository, and a ToDoovy demo as a CodePen. Then a set of live CodePen demos that cover a cancelable countdown, an order lookup, a cached Ajax example, and two event stream demos. Then the reactive side, with an Event Stream demo using the IOx Reactive Monad, a standalone IOx Reactive Monad demo, an IOx Reactive Monad stream operations demo, and two IOx countdown timer demos, one of which is described as the alternative.
The pairing of a straightforward IO demo with an IOx demo of the same shape is a useful signal. IOx is the reactive variant, and the library ships it alongside the monadic one, so the same task can be written either as a chain of effects or as a reactive stream. The two countdown timer demos, one of each, are the clearest illustration of that choice being made in practice.
The philosophy behind the choice is stated in the Overview. Monio balances the power of monads, often dismissed by the non-FP programmer as academic and convoluted, while pragmatically embracing the reality of the vast majority of JS programs, which it characterises as paradigm mixture, some OO, some FP, and probably a lot of imperative procedural code.
The opening example shows the composition style, and it is worth reading because it uses three monads in one chain:
Just("Welcome, Monads")
.concat(Just(" And Friends"))
.map(v => v.toUpperCase())
.fold(Maybe.from)
.map(v => v + "!")
.fold(
() => IO.of("--empty--"),
greetings => IO(() => console.log(greetings))
)
.run();
// WELCOME, MONADS AND FRIENDS!Just is the Maybe monad in its present form, concat appends another Just, map transforms it, fold(Maybe.from) converts the Maybe into a fold function, and the final fold turns it into an IO that either logs an empty marker or logs the greeting. The whole chain is lazy until the trailing run call, and the run output is the concatenated uppercase string with an exclamation mark.
That is the style argument in one expression. Three monads, one chain, and the side effect happens exactly once, at the end.
Dual package publishing, a heavy test matrix, and a library with no releases
The packaging tells you this is a mature npm package rather than a weekend experiment. The badge at the top of the documentation says the modules are ESM plus UMD plus CJS, and the package.json export map backs that up with a subpath export for the root, for just, for nothing, for maybe and more, each with an import entry pointing at a built file in dist/esm and a default entry pointing at the source file in src.
The main field points at ./src/index.js, so a CommonJS consumer requiring the package gets the source, while the exports map's import condition gets the ESM build. That split is a deliberate accommodation for bundlers and for Node's own module resolution, and it means the published tarball carries both a built ESM tree and the sources.
The build is a script, node scripts/build-all.js, and the tool that produces the individual module files is moduloze, which is why the mz-dependencies block in package.json maps output paths to module names, with index.js to Monio, just.js to Just, io/io.js to IO and so on. The build is not a bundler, it is a file splitter, which is what keeps the per module entry points possible.
The test story is the most elaborate part of the package.json. There is a test script, a test:package variant, test:umd and test:esm variants, and a test:all that chains all of them plus a scripts/test-all-complete script. The test script runs node with --trace-warnings and --max-old-space-size=2000, and the coverage script uses nyc with an explicit set of excludes for the tests, node-tests.js and load-tests.js directories, also with the memory flag. The test framework is QUnit, with sinon for stubs and rxjs as a dev dependency, which suggests the reactive IOx implementation is tested against an independent reactive implementation.
The memory flag is the detail to notice. Capping the old space at 2000 MB and tracing warnings is what you do when you suspect a test is leaking or that a promise is not settling, and Monio's test suite is built around lazy IO chains and promise resolution, so a hanging chain shows up as a process that never exits.
There are no GitHub releases for this project, and the package version in package.json is 0.71.0. The repository is not archived, and the last push was on 2026-05-01, so the branch is receiving work, but the release signal is on npm rather than in the repository. The top level listing also has MONIO.md, MONADS.md, a .mzrc file for the build tool, a scripts/ directory, src/ and tests/ directories, a .travis.yml, and a LICENSE.txt with the MIT licence.
Editorial conclusion
Monio fits a JavaScript developer who wants side effects modelled explicitly with a lazy IO monad, who likes generator based do-notation, and who wants exceptions and Either results to compose with the same chain rather than living in separate error handling paths. It is the wrong tool if your team is not comfortable with monads, since the documentation itself is a multi-chapter tutorial before it gets to the API, and the wrong tool if you need a Future or Observable monad instead, because Monio deliberately models asynchrony over promises. Before adopting, read MONIO.md for the full monad list and MONADS.md for the theory, and check the last push date, which was 2026-05-01, against your own tolerance for a library whose headline capability claim is made by its author rather than by a specification.
Frequently asked questions
What is Monio and what does the IO monad give me in JavaScript?
Monio provides an async-capable IO monad with do-style syntax, plus companion Maybe, Either and State monads and reactive IOx variants. IOs are lazy, so operations are not triggered until run is called, and Monio's IO is pre-transformed for promises, so promises produced inside a chain are unwrapped and the run result is lifted to a promise when any step is async.
What is the difference between yield and await in Monio's do-style syntax?
Yield is for chaining IOs, which can produce promises to defer, while await is for explicitly deferring on a promise that is not already wrapped in an IO. The do method accepts JS generators including async generators, and it automatically lifts the IO to promise-producing asynchrony.
Does Monio use promises or a Future monad?
Promises, deliberately. The README says Monio intentionally chooses to model asynchrony over promises instead of Future monads, because of its goal of balancing FP with pragmatic and idiomatic non-FP JS, while noting that nothing should stop you from using a Future monad with Monio if you prefer.
How do I handle errors in a Monio IO chain?
Two ways. Uncaught JS exceptions become promise rejections and are try and catch able, and IO-produced promise rejections work the same way. Separately, doEither transforms uncaught exceptions into Either:Left values and recognises IO-produced Either:Left values as try and catch able exceptions.
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/getify-monio)Star history chart for your README
[](https://hysenlabs.com/projects/getify-monio)Was this useful?