MoonBit: A Modern Language for WebAssembly/JS/Native

MoonBit: A Modern Language for WebAssembly/JS/Native

Source: Dev.to

Why MoonBit? ## Limitations of TypeScript ## Rust is Too Low-Level for Applications ## Installation ## Quick Start ## Language Basics ## Functions and Pipelines ## Structs with Labeled Arguments ## Enums as Algebraic Data Types ## Error Handling ## Async Support (Experimental) ## For Loops and Iterators ## Build Targets ## Built-in Testing ## Package Management ## moon CLI Features ## Learning Resources ## Official Tour ## Language Documentation ## Weekly Updates ## Source Code ## Practical Examples ## My Hands-on Experience ## Is It Ready for Production? ## Language Specification ## Async Runtime ## Conclusion ## About This Article 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. https://www.moonbitlang.com/ 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: What Makes MoonBit Great: TypeScript, being built on JavaScript, has fundamental constraints: Rust excels at systems programming, but lifetime management creates friction for GUI/frontend development. Consider this requestAnimationFrame example with wasm-bindgen: MoonBit offers Rust's expressiveness without this overhead. Try it in the browser first: https://try.moonbitlang.com/ For local development: https://www.moonbitlang.com/download/ Install the VS Code extension for LSP support: https://marketplace.visualstudio.com/items?itemName=moonbit.moonbit-lang Create and run a project: 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. Pattern matching lets you extract and process values simultaneously. MoonBit uses explicit error declarations with raise: Functions that can raise errors must explicitly declare it, and callers must handle them. MoonBit uses a coroutine-based approach similar to Kotlin. Async functions are declared with the async keyword and implicitly raise errors. This feature is still experimental but works with the JS backend. Here's how to implement a sleep function by wrapping JavaScript's setTimeout: 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. 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+. Set project default in moon.mod.json: MoonBit has a powerful built-in test runner: MoonBit uses mooncakes.io as its package registry: Import packages in moon.pkg.json: Use in code with @ prefix: 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. The moon command provides many useful features: Interactive tutorial covering the basics: https://tour.moonbitlang.com/ Comprehensive language reference: https://docs.moonbitlang.com/en/latest/language/index.html The most reliable source for new features: https://www.moonbitlang.com/weekly-updates/ Both the compiler and CLI are open source: Computer science topics implemented in MoonBit: https://www.moonbitlang.com/pearls/ After building React SPAs and various tools in MoonBit, here's what I've found: Project Structure Notes: Currently in beta, with version 1.0 stable release planned for 2026. Upcoming major changes include: Past breaking changes included: 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. 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. Currently async test works with --target native. The team is also working on --target js support. 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. 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: 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! Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: 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!") }) } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: 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!") }) } CODE_BLOCK: 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!") }) } COMMAND_BLOCK: #[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(()) } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: #[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(()) } COMMAND_BLOCK: #[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(()) } COMMAND_BLOCK: # macOS / Linux curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash # Upgrade moon upgrade Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: # macOS / Linux curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash # Upgrade moon upgrade COMMAND_BLOCK: # macOS / Linux curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash # Upgrade moon upgrade COMMAND_BLOCK: $ moon new hello $ cd hello $ moon run main Hello, World! Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: $ moon new hello $ cd hello $ moon run main Hello, World! COMMAND_BLOCK: $ moon new hello $ cd hello $ moon run main Hello, World! COMMAND_BLOCK: 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) } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: 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) } COMMAND_BLOCK: 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) } COMMAND_BLOCK: 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 }") } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: 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 }") } COMMAND_BLOCK: 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 }") } COMMAND_BLOCK: 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") } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: 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") } COMMAND_BLOCK: 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") } COMMAND_BLOCK: // 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) } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // 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) } COMMAND_BLOCK: // 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) } COMMAND_BLOCK: // 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!") } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // 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!") } COMMAND_BLOCK: // 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!") } CODE_BLOCK: 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]") } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: 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]") } CODE_BLOCK: 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]") } COMMAND_BLOCK: # 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 Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: # 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 COMMAND_BLOCK: # 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 CODE_BLOCK: { "name": "myproject", "preferred-target": "js" } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: { "name": "myproject", "preferred-target": "js" } CODE_BLOCK: { "name": "myproject", "preferred-target": "js" } CODE_BLOCK: // 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]") } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // 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]") } CODE_BLOCK: // 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]") } COMMAND_BLOCK: moon test # Run all tests moon test -u # Update snapshots moon test --target js # Test JS backend Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: moon test # Run all tests moon test -u # Update snapshots moon test --target js # Test JS backend COMMAND_BLOCK: moon test # Run all tests moon test -u # Update snapshots moon test --target js # Test JS backend COMMAND_BLOCK: # Add a package moon add username/package # Publish your package moon publish Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: # Add a package moon add username/package # Publish your package moon publish COMMAND_BLOCK: # Add a package moon add username/package # Publish your package moon publish CODE_BLOCK: { "import": [ "moonbitlang/x/json" ] } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: { "import": [ "moonbitlang/x/json" ] } CODE_BLOCK: { "import": [ "moonbitlang/x/json" ] } CODE_BLOCK: fn main { let json : Json = @json.parse("{\"key\": \"value\"}").unwrap() println(json.stringify()) } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: fn main { let json : Json = @json.parse("{\"key\": \"value\"}").unwrap() println(json.stringify()) } CODE_BLOCK: fn main { let json : Json = @json.parse("{\"key\": \"value\"}").unwrap() println(json.stringify()) } COMMAND_BLOCK: 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 Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: 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 COMMAND_BLOCK: 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 CODE_BLOCK: async test { @async.sleep(100) } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: async test { @async.sleep(100) } CODE_BLOCK: async test { @async.sleep(100) } - 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 - Still in beta - expect breaking changes (1.0 planned for 2026) - Limited ecosystem and third-party libraries - Async support is experimental - Requires developer capability to fill gaps - 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 - %async.run: Spawn and run a new coroutine - %async.suspend: Suspend the current coroutine, with resume managed via callback - Compiler (OCaml): https://github.com/moonbitlang/moonbit-compiler - CLI (Rust): https://github.com/moonbitlang/moon - Core library: https://github.com/moonbitlang/core - 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. - 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 Functor or Monad. - LSP can become unstable when heavily using extern for FFI and switching backends. - All directories are controlled with moon.pkg.json (migrating to moon.pkg DSL). Files in a directory share the same namespace with no file scope. - Imported libraries are accessed with @ prefix: mizchi/js becomes @js.something(). - Iter deprecation in favor of Iterator - moon.pkg.json migration to moon.pkg DSL - Exception syntax: ()-> T!E → ()->T raise E - Import syntax: fnalias → using statements - 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