iTranslated by AI
MoonBit is Awesome 2025
Are you frustrated that TypeScript's language specifications are fundamentally unstable because they originate from JS, or that Rust is too low-level for writing the application layer?
MoonBit is a language that solves these frustrations. However, currently, it is based on the premise that you write everything yourself without relying on the power of an ecosystem.
This article reflects my impressions of MoonBit as of November 2025.
Compared to April 2024, it has evolved significantly with native backend support, a built-in JSON type, exception support, and asynchronous support.
I thought, "Is it finally ready for practical use?", and I managed to write React bindings using MoonBit and achieve a functioning SPA. This article includes my thoughts from that experience.
Why MoonBit is great
MoonBit solves many of the frustrations I feel with TypeScript.
- Rust-like syntax static typed functional language
- Pattern matching
- Expression-oriented: if, match, and for-else are expressions
- F# style pipeline syntax
- Explicit side-effect control
- Enums that can be used as Algebraic Data Types (ADT)
- Completion and refactoring via LSP
- Explicit exception handling
- Asynchronous (async) support
- Built-in JSON type / JSON pattern matching
- Support for wasm/js/native/llvm backends
- Built-in test runner / snapshots
- Small generated code size!
The small size of the generated code is what makes me the happiest; it's realistic to write libraries for npm using MoonBit.
I will explain these benefits with some sample code.
If you want to run it locally, please install it first and then try generating a boilerplate with something like moon new myapp.
Function Pipeline and Built-in Test Runner
fn add(a: Int, b: Int) -> Int {
a + b // return the last expression
}
// Tests can also be written inline
test "add test" {
// Normal style
assert_eq(add(1, 2), 3)
// Pipeline style: the previous expression is passed as the first argument
1 |> add(2) |> assert_eq(3)
}
You can run this code with moon test. You can specify the backend with --target; by default, it is equivalent to moon test --target wasm-gc.
You can specify the project default using preferred-target in moon.mod.json.
Structs
// Struct declaration
struct Point {
x: Int
y: Int
} derive(Show, Eq) // Automatically implements equal and to_string
// Function declaration. a~ is a keyword argument, and b?: is an optional keyword argument
fn Point::new(a~: Int, b?: Int = 0) -> Point {
{a, b} // Shorthand for Point::{ a: a, b: b } with inference
}
fn Point::get_x(self: Self) -> Int {
self.x
}
test "test Point" {
let p = Point::new(a=1)
// The content of inspect is an inline snapshot that
// can be updated with moon test -u
inspect(Point::{a: 1, b: 0}, content="")
}
It is comfortable that inline snapshots are built into the language from the start.
By implementing Point::to_string(self: Self) with derive(Show), it satisfies the arguments for inspect(value: &Show, content~: String), which matches as a string.
Unused code tracking is also excellent; in this case, an unused warning appears for Point::get_x.
Enums that can be used as Algebraic Data Types
enum CounterAction {
Increment
Add(Int)
}
test "test match" {
// Inferred as Array[CounterAction]
let actions = [
CounterAction::Increment,
CounterAction::Add(3)
]
// Explicitly Mutable
let mut v = 0
// for loop
for action in actions {
// Since it's a match expression, it can return a value
v += match action {
// CounterAction:: can be omitted when matching
Increment => 1
Add(n) => n
// All patterns are covered, so it gives an unreachable warning
_ => panic()
}
}
assert_eq(v, 4)
}
You can process values while extracting them using pattern matching with the match expression.
Explicit Exception Handling and Async
Functions that may cause errors must declare them explicitly. If the caller doesn't have a corresponding error raise, it must handle the error explicitly.
// Error type declaration
suberror DivByZeroError
fn div(a: Int, b: Int) -> Int raise DivError {
if b == 0 {
raise DivByZeroError
}
a / b
}
test "test error" {
// In a noraise function, all errors must be handled
let f: () -> noraise = () => {
// catch is pattern matching
let _ = try div(1, 0) catch {
DivByZeroError => println("divide by zero error")
}
}
f()
}
Asynchronous code has many prerequisites and is complex, so I'll omit the details, but you can write something like this:
async fn foo() -> Unit raise {
@async.sleep(1000) // await is not required
}
Since the async status is known from the caller's async context, await is not necessary on the calling side. The raise declaration propagates.
One thing I found a bit confusing while researching is that in asynchronous functions, async fn(){} is equivalent to raise Error (raising the base Error type) by default, while synchronous functions fn(){} are noraise by default.
Learning Resources
Language Tour
It is best to start with the official tour.
While not exhaustive, it is helpful for getting an overview.
Language Specification Documentation
Weekly Update
The most reliable sources of information are the Weekly Updates and the implementation of moonbitlang/core.
The built-in library moonbitlang/core is written in MoonBit itself and always follows the latest language specifications.
Source Code
Compiler implementation (OCaml)
moon CLI implementation (Rust)
Mooncakes: Package Registry
Packages from core developers such as moonbitlang, bobzhang, tonyfettes, peter-jerry-ye, and illusory0x0 are of relatively high quality.
Older packages often lack backward compatibility and are likely to be left non-functional.
Practical Practice Collection
Rather than explanations of MoonBit itself, there are many articles on practicing computer science using MoonBit.
My Personal Feel for MoonBit
- Unlike last year, I no longer encounter situations where I can't write something no matter how hard I try due to a lack of language features.
- A language with a solid LSP toolchain, pattern matching, and pipelines is simply the best.
- I recently tried OCaml, F#, and Haskell, and MoonBit is the only one that provided a sense of security in its toolchain comparable to TypeScript (Node/npm) or Rust (Cargo).
- However, when using
externextensively for FFI and building for different backends, there are times when the LSP behavior becomes a bit unstable.
- While type inference allows for writing complex code concisely, it can also lead to higher costs when reading the final code.
- This is similar to the difficulty encountered when making full use of pattern matching in Haskell.
- Explicit exceptions feel overly complex in practice.
- I understand them now, but I still often find it challenging to wrap logic in
raise/noraisefunctions when type errors occur.
- I understand them now, but I still often find it challenging to wrap logic in
- When designing seriously, you need to think in terms of Rust-like traits and enums rather than TypeScript-style Unions.
- Currently, trait declarations cannot take type parameters, which limits the capabilities of traits.
- You may still encounter bugs in edge cases for each backend. Examples I faced:
- Calling a function of type
f: () -> Unit raise?inside a loop caused a crash. - Making a symbol with the same name as a built-in symbol public, such as
pub enum Option {...}, causes the test runner to crash.
- Calling a function of type
- Project structure is managed by placing a
moon.pkg.jsonin every directory..mbtfiles within the same directory share the same namespace and do not have file scope. - Imported external libraries are accessed by appending
@to the end of the package path.- Example:
mizchi/jsis accessed like@js.new_empty_object().
- Example:
So, Is It Usable?
The language specification is maturing, but...
Currently, MoonBit is in beta, and language specifications are still frequently being added or changed.
For example, exception declarations changed from () -> T!E to () -> T raise E, and fnalias @foo.bar as baz was replaced by the using statement: using @foo { bar as baz }.
However, these are not deprecated immediately. Formatting with moon fmt will automatically convert some of them, or build warnings will appear for a certain period before they are officially removed.
The major upcoming change announced is that moon.pkg.json will become an in-language DSL called moon.pkg, but this is also expected to be migratable using the formatter.
The most drastic change I've seen in the past was when the inference for the default array literal let arr = [1, 2, 3] changed from immutable to mutable, but I haven't seen changes of that scale recently.
That said, you are essentially responsible for any behavior not explicitly documented, which you must figure out from the implementation.
According to the official roadmap, version 1.0 is scheduled for release in 2026. It might be wise to wait until then.
Async/Native Instability
In modern development, asynchronous support is necessary to write real-world applications, but here it's more the implementation than the specification that is unstable.
Currently, the official library moonbitlang/async is under active development.
This library is designed for the native backend and is not just a simple asynchronous utility; it wraps Unix system calls at a low level, making it equivalent to what Tokio is for Rust.
I've tried it locally several times, but the API changes every time I touch it, and it hasn't stabilized yet.
As an experimental feature, moon test allows the use of async test only when using --target native and having moonbitlang/async as a dependency.
async test {
@async.sleep(100)
}
Additionally, there is a library called maria. This is a version of the AI coding agent MoonPilot rewritten in MoonBit, and it appears that dogfooding of async and native features is happening there and being reflected back into the async library.
According to an official announcement on X, they are working on async test support for --target js. As someone who writes asynchronous code for the JS backend, I'd like to wait for this before considering it ready for practical use.
Conclusion
If you don't mind the rework caused by specification changes or shifts in the ecosystem, it's a language worth investing in even now.
What's missing is the ecosystem, but since that's a chicken-and-egg problem, I've decided to address it by writing plenty of JS bindings while waiting for it to gain popularity.
I spent most of 2025 writing code with AI, but thanks to MoonBit, I feel like I've remembered the joy of programming. By intentionally writing in a language with fewer learning resources where AI is less proficient, I feel I've regained some of my programming strength.
Discussion