Functions

Contents

Typed functions use this shape:

pub fn name([env: napi.Env,] ...js_args) Return

An Env in the first position is injected automatically and does not consume a JS argument. Everything else is converted from the JS arguments at the call site, and the return value is converted back. Raw call inspection is a separate, exact signature described below.

The progression

Three flavors of function, in order of how often you need them.

Just values

The simplest case: take JS arguments, return a value. No environment needed.

pub fn add(a: i32, b: i32) i32 {
    return a + b;
}
addon.add(2, 3); // 5

If your function does not allocate, throw, or call back into JS, this is all you need. add(i32, i32) does not even touch the allocator.

Take an Env

Add env: napi.Env as the first parameter when you need to allocate, build complex JS values, or throw.

pub fn greet(env: napi.Env, name: []const u8) ![]const u8 {
    return std.fmt.allocPrint(env.allocator(), "Hello, {s}!", .{name});
}

env.allocator() is an arena that resets when your function returns. See Memory model.

Take raw CallInfo

For variadic or dynamic-arity functions, use exactly (napi.Env, napi.CallInfo) !napi.Val and pull out the arguments yourself. Raw mode owns all argument and result conversion, so it cannot be combined with additional typed parameters or another return type.

pub fn sum(env: napi.Env, info: napi.CallInfo) !napi.Val {
    const args = try info.argsSlice(env, 16);
    var total: f64 = 0;
    for (args) |arg| total += try arg.to(env, f64);
    return env.toJs(total);
}
addon.sum(1, 2, 3, 4); // 10

This is the escape hatch when the static type system cannot describe what you want. argsSlice safely caps the captured values at its comptime maximum even when the caller passes more arguments.

Returning your own JS values

For values the auto-converter cannot build (Buffers, dynamic-key objects, hand-built arrays), return !napi.Val and construct it yourself:

pub fn makeBuffer(env: napi.Env, size: u32) !napi.Val {
    const buf = try env.createBuffer(size);
    @memset(buf.data, 0xff);
    return buf.val;
}

pub fn getInfo(env: napi.Env) !napi.Val {
    const obj = try env.createObject();
    try obj.setNamedProperty(env, "name", try env.toJs("@teakit/napi"));
    try obj.setNamedProperty(env, "version", try env.toJs(1));
    return obj;
}

napi.Val is a passthrough type. Anywhere a JS value is expected (return values, callback arguments, struct fields), you can substitute a Val and it travels through unconverted.

Naming

Field and function names are translated from snake_case to camelCase automatically. Both forms work in the published .d.ts:

pub fn read_file(path: []const u8) ![]const u8 { ... }
addon.readFile("/etc/hosts");

If you want a JS-visible name that is not a valid Zig identifier, build the export manually with env.createObject() and setNamedProperty.

Automatic exports must also be valid JavaScript binding identifiers after camel-casing. Two declarations such as read_file and readFile are rejected at compile time because both would become readFile; reserved words and names that cannot be emitted safely by the ESM wrapper are rejected as well. The same check applies independently inside every namespace.

What's next?

  • Namespaces explains how pub const x = struct { ... } becomes a nested object.
  • Type conversion is the table of every Zig type and what it maps to in JS.
  • Errors covers throwing, rejecting, and the napi.Error set.