Salam vs Zig is a comparison between two young systems languages that look alike at first glance. Zig is a low-level language designed as a better C: no hidden control flow, no hidden allocations, explicit allocators passed to whatever needs memory, and comptime, which runs ordinary Zig code at compile time. Salam is a statically typed systems language with a block syntax closer to Python or Lua, a large application-level standard library and source that can be written in English or Persian. Both compile to native code, neither has a garbage collector, and both are still before their 1.0 release.
This article compares how they handle errors, memory, compile-time code, C libraries and cross-compilation, with the same small programs written in both. The Salam programs are compiled and run when this site is built. The Zig programs follow the 0.15 standard library, and since Zig is pre-1.0, details may change in later versions.
Two young languages with a lot in common
Zig first appeared in 2016, created by Andrew Kelley, and is developed by the Zig Software Foundation. It already has well-known users, among them the Bun JavaScript runtime, the TigerBeetle database and the Ghostty terminal. Salam is younger and its compiler has been written in Salam itself since the original C implementation was retired in August 2026. Both are open source.
| Zig | Salam | |
|---|---|---|
| Design goal | A simpler, safer replacement for C | A readable systems language for apps, servers and low-level code |
| Syntax | C-like, braces and semicolons | Blocks with : and end, no semicolons, English or Persian keywords |
| Memory | Explicit allocators, passed as arguments | One global allocator for collections; mem arenas and pools on request |
| Errors | Error unions (!T), try, catch, errdefer | throws, try, catch; errors carry a kind, a message and a position |
| Compile time | comptime: run any Zig code while compiling | Constants, conditional compilation, -D flags, monomorphized generics |
| C interop | Imports C headers; zig cc compiles C and C++ | extern: declarations; calls follow the C ABI on every backend |
| Standard library | Lower level: data structures, I/O, HTTP, JSON, crypto | Application level: HTTP router, databases, JSON, TLS, images, ML |
| Status | Pre-1.0, breaking changes between releases | Pre-1.0, breaking changes between releases |
Error handling: error unions and throws
The two languages ended up with surprisingly similar error handling. Here is a function that parses a TCP port in each, and a loop that reports good and bad inputs. In Zig, a function that can fail returns an error union, try passes an error up, and if/else with captures handles both outcomes:
const std = @import("std");
fn parsePort(text: []const u8) !u16 {
const n = try std.fmt.parseInt(u32, text, 10);
if (n < 1 or n > 65535) return error.OutOfRange;
return @intCast(n);
}
pub fn main() void {
const inputs = [_][]const u8{ "8080", "70000", "http" };
for (inputs) |text| {
if (parsePort(text)) |port| {
std.debug.print("port {d}\n", .{port});
} else |err| {
std.debug.print("bad: {s}\n", .{@errorName(err)});
}
}
}In Salam, a function that can fail is marked throws, every call to it is marked try or sits inside a try block, and catch handles the error:
import conv
func parse port(text: str): int throws:
n := try conv.Atoi(text)
if n < 1 or n > 65535:
throw Error.New(ErrorKind.OutOfRange, "port out of range: " + text)
end
ret n
end
func main:
each text in ["8080", "70000", "http"]:
try:
println "port", parse port(text)
catch e:
println "bad:", e.message
end
end
endThe shape is the same: no exceptions, no stack unwinding, every failure point visible at the call site, and defer statements still run. Both also have a one-line fallback: Zig writes parsePort(text) catch 8080, Salam writes try parse port(text) catch 8080.
The difference is what an error carries. A Zig error is a name from an error set, and that is all; extra details travel through a separate diagnostics value when you need them, and Zig's error return traces show where an error came from in debug builds. A Salam error is a value of the built-in Error type with a kind, a message, an optional code and, for parsers, the line and column, so json: missing key 'age' (line 1, column 1) comes straight out of the standard library. Zig has errdefer, which runs only when the function returns an error; Salam has no equivalent and uses defer or an explicit catch.
Memory: explicit allocators vs defer and free
Allocators are Zig's signature feature. Any function or container that allocates takes an std.mem.Allocator, so the caller decides whether memory comes from a general-purpose heap, an arena, a fixed buffer or a testing allocator that reports leaks. Since Zig 0.15 the standard ArrayList does not even store its allocator; every call that may allocate receives it:
const std = @import("std");
pub fn main() !void {
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var squares: std.ArrayList(usize) = .empty;
defer squares.deinit(allocator);
for (1..6) |i| {
try squares.append(allocator, i * i);
}
std.debug.print("{any}\n", .{squares.items});
}Salam makes the opposite trade. Collections such as Vector and HashMap use one global allocator, so there is no allocator argument anywhere, but freeing is still explicit, with the same defer idiom:
func main:
mut squares := Vector {} as Vector<i64>
defer squares.free()
repeat 1 to 5 in i:
squares.push((i * i) as i64)
end
println squares
endSalam's version is shorter because it decides for you. When you need arenas or fixed-size pools, the mem package has NewArena, ArenaAlloc, ArenaReset and NewPool, but they hand out raw memory rather than plugging into the collections the way a Zig allocator does. If your program must control every allocation, for example in an embedded system or a database with a strict memory budget, Zig's model is more flexible.
For finding mistakes, Zig's DebugAllocator reports leaks when it is deinitialized, and Debug and ReleaseSafe builds check bounds and integer overflow at runtime. Salam checks array and vector indices at runtime in normal builds (not with --release), proves some indices out of range while compiling, reports some use-after-free and double-free patterns before the program runs, and salam memcheck runs a program under Valgrind or AddressSanitizer. Neither language proves memory safety the way Rust's borrow checker does. The article Memory management in Salam covers Salam's side in depth.
comptime vs Salam's compile-time features
comptime is where Zig is in a different league. A Zig function can run at compile time when its arguments are known, types are values that functions can take and return, and that is how Zig does generics: ArrayList(u64) is a function call that returns a type. Platform choices use the same mechanism:
const builtin = @import("builtin");
const config_dir = if (builtin.os.tag == .windows) "AppData" else ".config";Salam has a set of narrower tools instead of one general one. A top-level if on a compile-time flag keeps only the branch for the target platform, -DNAME adds your own flags, -dNAME=value adds constants, and generics are specialized per type at compile time:
if SALAM_OS_WINDOWS:
const CONFIG_DIR := "AppData"
else:
const CONFIG_DIR := ".config"
end
func main:
println "config lives under", CONFIG_DIR
println "built for", SALAM_OS, "on", SALAM_ARCH
endThe output depends on the machine the program is built for, so it is only checked here; on an x86-64 Linux machine it prints .config and built for linux on x64. Constant expressions fold at compile time, and the compiler does a lot of checking at that stage (exhaustive match, index ranges, unused names, the HTML and CSS of the page language). But there is no general way to run your own functions while compiling, and generic code cannot inspect types the way @typeInfo lets Zig code do. If you want to generate serializers, parsers or lookup tables from types inside the language, Zig can and Salam cannot. Salam's JSON codecs are derived by the compiler itself rather than by library code. The conditional compilation lesson lists Salam's flags.
C interop
Both languages treat C as a first-class neighbor. Zig can import a C header directly and call its functions with no wrapper, and its toolchain doubles as a C and C++ compiler (zig cc):
const std = @import("std");
const c = @cImport(@cInclude("stdlib.h"));
pub fn main() void {
std.debug.print("{d} {d}\n", .{ c.abs(-7), c.atoi("42") });
}Salam does not read headers. You declare the C functions you need in an extern: block, with Salam types that match the C ones, and call them like any other function:
extern:
func abs(x: int): int
func atoi(s: str): int
end
func main:
println abs(-7), atoi("42")
endZig's header import is more convenient for a large C API, because you do not retype the declarations. Salam's explicit declarations are more work but make the boundary visible in one place. In the other direction, Salam's export: block publishes functions under plain C names, and salam build --lib builds a shared or static library together with a generated header. Structs pass by value with the platform's C ABI. The C interop lesson and Calling C from Salam cover the details, including link dynamic for libraries.
Cross-compilation and toolchains
Cross-compilation is one of Zig's best features. zig build and zig cc can target many operating systems and architectures from any host, and Zig ships the libc headers and sources it needs, which is why some projects in other languages use Zig only as their C cross-compiler.
Salam cross-compiles with a --target triple, for example salam build app.salam --target=x86_64-w64-windows-gnu. Release builds of the compiler embed LLVM and its linker, and the native backend writes object files itself for x86-64, AArch64, 32-bit x86 and ARM and RISC-V 64, with a built-in linker for some targets. WebAssembly (wasm32-wasi) and freestanding kernels (*-unknown-none) are targets too. Zig's setup is more mature and covers more targets with less configuration. Cross-compiling Salam shows what works today and what each target needs.
Performance
The project's benchmark compares Salam with C, C++, Rust, Go, Python and PHP, but not with Zig, so there is no measured comparison to report here. What can be said: Zig compiles through LLVM for optimized builds, like Salam's release path, and both sit in the class of compiled languages without a garbage collector. On the 39 same-algorithm programs Salam averaged 10.6 ms against 11.7 ms for C and 11.3 ms for Rust, which the page reads as the same range, with Salam ahead on 6 programs, level on 27 and behind on 6. The programs are short, start-up is part of each time and gaps under about 5% are noise. Expect Zig and Salam to land close to each other on similar code, and measure if it matters.
Ecosystem and stability
Both languages change between releases, and both tell you so. Zig's 0.15 release, for example, reworked the standard library's reader and writer interfaces, and code written for earlier versions had to be updated. Salam removed typed local declarations in 2026 and changes its rules from release to release as well.
The ecosystems differ in kind. Zig has a growing community, a package manager built into its build tool and several large open source projects that prove it in production. Salam's community is much smaller and its third-party package registry is new and nearly empty, but its standard library is far larger at the application level: HTTP servers with routing and middleware, database drivers, JSON, YAML and TOML, TLS written in Salam, image codecs, matrices and neural network layers, and a page language that generates HTML and CSS. Zig gives you sharp building blocks and expects you to assemble them; Salam gives you more finished parts.
When to choose which
Choose Zig when:
- you need precise control over every allocation, with allocators chosen per call;
- you want compile-time code generation and type reflection inside the language;
- you are replacing or extending a C codebase and want to import its headers directly;
- you need cross-compilation to many targets with minimal setup, or a drop-in C compiler.
Choose Salam when:
- you are writing services, command-line tools or web backends and want the standard library to do most of the work;
- you prefer a higher-level, readable syntax with enums that carry data, exhaustive
matchand interfaces; - you want one toolchain with tests, formatting, packages, a language server and a page language;
- you or your team want to write code in Persian as well as English.
Frequently asked questions
Is Salam like Zig?
They share goals: native code, no garbage collector, explicit errors with try, and defer for cleanup. They differ in philosophy: Zig makes memory and compile-time code explicit and programmable, while Salam hides the allocator and offers a larger standard library and a higher-level syntax.
Does Salam have comptime?
Not in Zig's sense. Salam evaluates constant expressions, keeps or drops code with compile-time flags and specializes generics per type, but it cannot run arbitrary functions while compiling.
Does Salam use allocators like Zig?
No. Collections use a global allocator and are freed with .free(), usually through defer. The mem package offers arenas and pools for raw memory when you need them.
Which is faster, Salam or Zig?
There is no published benchmark that compares the two. Both compile through LLVM for optimized builds and have no garbage collector, so they should land in the same range on similar code.