Are you frustrated with TypeScript inheriting JavaScript's quirks - no integer types, type erasure at runtime, no pattern matching? Or that Rust is too low-level for writing application-layer code?
MoonBit is a language that addresses these frustrations. It's a statically-typed programming language designed for WebAssembly, with multiple backend targets including wasm-gc, JavaScript, and native. Think of it as Rust with garbage collection - you get Rust-like syntax and safety without the complexity of lifetime management.
This article provides a comprehensive introduction to MoonBit, including my hands-on experience building React SPAs with it. The caveat is that, for now, you need to be prepared to write some things yourself without relying heavily on the ecosystem.
https://github.com/mizchi/js.mbt
Here's a taste of what JS interop looks like with my library:
fn main {
// Get DOM element and modify it
let doc = @dom.document()
let el = doc.createElement("div")
el.setId("app")
el.setTextContent("Hello from MoonBit!")
doc.body().appendChild(el)
// Add event listener
el.addEventListener("click", fn(_ev) {
@js.console_log("Clicked!")
})
}
tl;dr
What Makes MoonBit Great:
- Rust-like syntax with GC - easy to learn, no lifetime complexity
- Pattern matching and algebraic data types (enums)
- Expression-oriented (if, match, for are expressions)
- F#-style pipeline operator (
|>) - Explicit side-effect control and exception handling
- Built-in JSON type with pattern matching
- Multiple backends: wasm-gc, JavaScript, native
- Built-in test runner with inline snapshot testing
- Small generated code size - realistic for npm-publishable libraries
- Complete toolchain: LSP, formatter, package manager
Current Limitations:
- Still in beta - expect breaking changes (1.0 planned for 2026)
- Limited ecosystem and third-party libraries
- Async runtime is being integrated into core
- Requires developer capability to fill gaps
Why MoonBit?
Limitations of TypeScript
TypeScript, being built on JavaScript, has fundamental constraints:
- No distinction between integers and floats (everything is Number)
- Types are erased at transpile time - the compiler can't use them for optimization
- No pattern matching
- Objects used as records lead to patterns like
type: "discriminator"everywhere
Rust is Too Low-Level for Applications
Rust excels at systems programming, but lifetime management creates friction for GUI/frontend development. Consider this requestAnimationFrame example with wasm-bindgen:
#[wasm_bindgen(start)]
fn run() -> Result<(), JsValue> {
let f = Rc::new(RefCell::new(None));
let g = f.clone();
*g.borrow_mut() = Some(Closure::new(move || {
// ... lifetime complexity everywhere
request_animation_frame(f.borrow().as_ref().unwrap());
}));
request_animation_frame(g.borrow().as_ref().unwrap());
Ok(())
}
MoonBit offers Rust's expressiveness without this overhead.
Installation
Try it in the browser first:
For local development:
https://www.moonbitlang.com/download/
# macOS / Linux
curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash
# Upgrade
moon upgrade
Install the VS Code extension for LSP support:
https://marketplace.visualstudio.com/items?itemName=moonbit.moonbit-lang
Quick Start
Create and run a project:
$ moon new hello
$ cd hello
$ moon run main
Hello, World!
Language Basics
Functions and Pipelines
fn add(a: Int, b: Int) -> Int {
a + b // last expression is returned
}
test "pipeline" {
// Pipeline passes result as first argument
let result = 1 |> add(2) |> add(3)
assert_eq(result, 6)
}
Structs with Labeled Arguments
struct Point {
x: Int
y: Int
} derive(Show, Eq)
// Labeled argument (x~), optional with default (y~)
fn Point::new(x~: Int, y~ : Int = 0) -> Point {
{ x, y } // shorthand for Point { x: x, y: y }
}
test "struct" {
let p = Point::new(x=10, y=20)
assert_eq(p.x, 10)
// inspect provides inline snapshot testing
// Run `moon test -u` to update the content
inspect(p, content="{ x: 10, y: 20 }")
}
Having inline snapshots built into the language from the start is remarkably convenient. derive(Show) auto-implements to_string, and unused code tracking warns you about unused functions.
Enums as Algebraic Data Types
enum Shape {
Circle(radius~ : Double)
Rectangle(width~ : Double, height~ : Double)
}
fn area(shape: Shape) -> Double {
match shape {
Circle(radius~) => 3.14159 * radius * radius
Rectangle(width~, height~) => width * height
}
}
test "enum" {
let circle = Shape::Circle(radius=5.0)
inspect(area(circle), content="78.53975")
}
Pattern matching lets you extract and process values simultaneously.
Error Handling
MoonBit uses explicit error declarations with raise:
// Declare error type
suberror DivError
fn div(a: Int, b: Int) -> Int raise DivError {
if b == 0 {
raise DivError
}
a / b
}
test "error handling" {
let result = try {
div(10, 2)
} catch {
DivError => -1
}
assert_eq(result, 5)
}
Functions that can raise errors must explicitly declare it, and callers must handle them.
Async Support
MoonBit uses a coroutine-based approach similar to Kotlin. Async functions are declared with the async keyword and implicitly raise errors.
Two core primitives:
-
%async.run: Spawn and run a new coroutine -
%async.suspend: Suspend the current coroutine, with resume managed via callback
Here's how to implement a sleep function by wrapping JavaScript's setTimeout:
// Declare external JS function
extern "js" fn js_set_timeout(f : () -> Unit, duration~ : Int) -> Unit =
#| (f, duration) => setTimeout(f, duration)
// Wrap as async function
async fn sleep(ms : Int) -> Unit {
await %async.suspend(fn(resume_ok, _resume_err) {
js_set_timeout(fn() { resume_ok(()) }, duration=ms)
})
}
async fn main {
await sleep(1000)
println("Done!")
}
For Promise integration, pass resume_ok as the resolve callback and resume_err as reject. The compiler tracks async-ness statically, and async function calls are shown in italics in the IDE.
For Loops and Iterators
test "loops" {
let arr = [1, 2, 3, 4, 5]
let mut sum = 0
// For-in loop
for x in arr {
sum += x
}
assert_eq(sum, 15)
// Functional style
let doubled = arr.map(fn(x) { x * 2 })
inspect(doubled, content="[2, 4, 6, 8, 10]")
}
Build Targets
MoonBit supports multiple backends. The wasm-gc target is now part of the WebAssembly baseline - supported in Chrome 119+, Firefox 120+, and Safari 18.2+.
# WebAssembly GC (default, baseline supported)
moon build --target wasm-gc
# JavaScript
moon build --target js
# Native binary
moon build --target native
# Run with specific target
moon run main --target js
# Test all targets
moon test --target all
Set project default in moon.mod.json:
{
"name": "myproject",
"preferred-target": "js"
}
Built-in Testing
MoonBit has a powerful built-in test runner:
// Inline tests
test "basic assertion" {
assert_eq(1 + 1, 2)
}
// Snapshot testing - run `moon test -u` to update
test "snapshot" {
let data = [1, 2, 3].map(fn(x) { x * x })
inspect(data, content="[1, 4, 9]")
}
Run tests:
moon test # Run all tests
moon test -u # Update snapshots
moon test --target js # Test JS backend
Package Management
MoonBit uses mooncakes.io as its package registry:
# Add a package
moon add username/package
# Publish your package
moon publish
Import packages in moon.pkg.json:
{
"import": [
"moonbitlang/x/json"
]
}
Use in code with @ prefix:
fn main {
let json : Json = @json.parse("{\"key\": \"value\"}").unwrap()
println(json.stringify())
}
Packages from the core team and active contributors (moonbitlang org and recognized maintainers) tend to be of high quality. Older packages may be broken due to ongoing language evolution.
moon CLI Features
The moon command provides many useful features:
moon check # Type check with lint warnings
moon check --deny-warn # Treat warnings as errors (for CI)
moon fmt # Format code
moon doc Array # Show type documentation
moon info # Generate type definitions
moon coverage analyze # Coverage report
moon bench # Run benchmarks
Learning Resources
Official Tour
Interactive tutorial covering the basics:
https://tour.moonbitlang.com/
Language Documentation
Comprehensive language reference:
https://docs.moonbitlang.com/en/latest/language/index.html
Weekly Updates
The most reliable source for new features:
https://www.moonbitlang.com/weekly-updates/
Source Code
Both the compiler and CLI are open source:
- Compiler (OCaml): https://github.com/moonbitlang/moonbit-compiler
- CLI (Rust): https://github.com/moonbitlang/moon
- Core library: https://github.com/moonbitlang/core
Practical Examples
Computer science topics implemented in MoonBit:
https://www.moonbitlang.com/pearls/
My Hands-on Experience
After building React SPAs and various tools in MoonBit, here's what I've found:
The Good:
- The LSP toolchain is solid. Pattern matching and pipelines make coding enjoyable.
- Among OCaml/F#/Haskell, MoonBit is the only one that gave me the same toolchain confidence as TypeScript or Rust.
- Type inference allows writing complex code concisely.
The Challenges:
- Type inference can make code harder to read - similar to heavily pattern-matched Haskell.
- Explicit exceptions feel complex in practice. Wrapping in raise/noraise functions takes getting used to.
- Design requires thinking in Rust-style traits and enums rather than TypeScript-style unions.
- No higher-kinded types yet - traits can't take type parameters, limiting abstractions like
FunctororMonad. - LSP can become unstable when heavily using
externfor FFI and switching backends.
Project Structure Notes:
- All directories are controlled with
moon.pkg.json(migrating tomoon.pkgDSL). Files in a directory share the same namespace with no file scope. - Imported libraries are accessed with
@prefix:mizchi/jsbecomes@js.something().
Is It Ready for Production?
Language Specification
Currently in beta, with version 1.0 stable release planned for 2026. Upcoming major changes include:
-
Iterdeprecation in favor ofIterator -
moon.pkg.jsonmigration tomoon.pkgDSL
Past breaking changes included:
- Exception syntax:
()-> T!E→()->T raise E - Import syntax:
fnalias→usingstatements
However, moon fmt automatically converts old syntax, and deprecations happen gradually with warnings. Compared to early beta, breaking changes have become much less frequent - MoonBit is now a practically usable language.
Async Runtime
The official moonbitlang/async library provides an async runtime similar to Rust's tokio. It wraps Unix system calls at a low level and works with the native backend. This library is planned to be integrated into the core language.
async test {
@async.sleep(100)
}
Currently async test works with --target native. The team is also working on --target js support.
Conclusion
MoonBit fills a unique niche: the expressiveness of Rust without lifetime complexity, targeting WebAssembly as a first-class citizen. The toolchain is polished, the language design is thoughtful, and the generated code is impressively small.
If you don't mind some churn from specification changes and ecosystem evolution, MoonBit is worth investing in now. What's lacking is the ecosystem, but that's a chicken-and-egg problem. I'm addressing it by writing JS bindings while waiting for adoption to grow.
About This Article
Written in December 2024. MoonBit is evolving rapidly - check the weekly updates for the latest changes.
I usually write articles in Japanese, but I wanted to introduce MoonBit to a broader audience. Despite its potential, I rarely see MoonBit mentioned on dev.to, X (Twitter), or other tech communities outside of Asia - which feels like a missed opportunity. The community is small but growing, and the development team is very responsive to feedback.
If you find MoonBit interesting, I encourage you to try it out and join the community:
- Discord: https://discord.gg/CVFRavvRav - The most active place for discussions
- GitHub Issues: https://github.com/moonbitlang/moonbit-docs/issues - Report bugs and request features
- Mooncakes: https://mooncakes.io/ - Publish your own packages
The ecosystem needs more libraries and tools. Whether you build something small or contribute documentation, every bit helps. I hope to see you in the community!
Top comments (0)