DEV Community

mizchi (Kotaro Chikuba)
mizchi (Kotaro Chikuba)

Posted on

MoonBit: A Modern Language for WebAssembly/JS/Native

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:

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

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(())
}
Enter fullscreen mode Exit fullscreen mode

MoonBit offers Rust's expressiveness without this overhead.

Installation

Try it in the browser first:

https://try.moonbitlang.com/

For local development:

https://www.moonbitlang.com/download/

# macOS / Linux
curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash

# Upgrade
moon upgrade
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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 }")
}
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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!")
}
Enter fullscreen mode Exit fullscreen mode

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]")
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Set project default in moon.mod.json:

{
  "name": "myproject",
  "preferred-target": "js"
}
Enter fullscreen mode Exit fullscreen mode

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]")
}
Enter fullscreen mode Exit fullscreen mode

Run tests:

moon test              # Run all tests
moon test -u           # Update snapshots
moon test --target js  # Test JS backend
Enter fullscreen mode Exit fullscreen mode

Package Management

MoonBit uses mooncakes.io as its package registry:

# Add a package
moon add username/package

# Publish your package
moon publish
Enter fullscreen mode Exit fullscreen mode

Import packages in moon.pkg.json:

{
  "import": [
    "moonbitlang/x/json"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Use in code with @ prefix:

fn main {
  let json : Json = @json.parse("{\"key\": \"value\"}").unwrap()
  println(json.stringify())
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

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 Functor or Monad.
  • LSP can become unstable when heavily using extern for FFI and switching backends.

Project Structure Notes:

  • 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().

Is It Ready for Production?

Language Specification

Currently in beta, with version 1.0 stable release planned for 2026. Upcoming major changes include:

  • Iter deprecation in favor of Iterator
  • moon.pkg.json migration to moon.pkg DSL

Past breaking changes included:

  • Exception syntax: ()-> T!E()->T raise E
  • Import syntax: fnaliasusing statements

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)
}
Enter fullscreen mode Exit fullscreen mode

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:

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)