Skip to content

Iterators and async iterators

napi-rs can make a native class implement JavaScript's synchronous or asynchronous iteration protocol. These APIs are currently marked experimental in the Rust source: test the exact napi-rs and runtime versions you publish, and expect refinements to trait or lifecycle behavior.

Rust marker and trait JavaScript protocol Cargo feature
#[napi(iterator)] + Generator or ScopedGenerator Symbol.iterator, next, return, throw Base napi API
#[napi(async_iterator)] + AsyncGenerator Symbol.asyncIterator, Promise-returning next, return, throw tokio_rt (or async)

The two marker attributes are mutually exclusive on one class. A marked class also cannot have public fields named next, return, or throw, because napi-rs installs those protocol methods.

Synchronous iterator

Implement Generator when yielded values are owned Rust values:

lib.rs
rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi(iterator)]
pub struct Counter {
  current: u32,
  end: u32,
}

#[napi]
impl Generator for Counter {
  type Yield = u32;
  type Next = u32;
  type Return = ();

  fn next(&mut self, value: Option<Self::Next>) -> Option<Self::Yield> {
    if let Some(next) = value {
      self.current = next;
    }
    if self.current >= self.end {
      return None;
    }
    let value = self.current;
    self.current += 1;
    Some(value)
  }
}

#[napi]
impl Counter {
  #[napi(constructor)]
  pub fn new(end: u32) -> Self {
    Self { current: 0, end }
  }
}
index.mjs
js
const counter = new Counter(3)

console.log(counter.next()) // { value: 0, done: false }
console.log(counter.next(2)) // { value: 2, done: false }
console.log(counter.next()) // { done: true }

console.log([...new Counter(3)]) // [0, 1, 2]

The generated declaration extends Iterator<Yield, Return, Next>.

Associated types

Associated type Required trait Used by
Yield ToNapiValue Converts Some(value) from next or catch to JavaScript.
Next FromNapiValue Converts the optional argument passed to iterator.next(value).
Return FromNapiValue Converts the optional argument passed to iterator.return(value).

The method receives Option<Next> because JavaScript may call next() with no argument. Some(yielded) produces { value: yielded, done: false }; None produces { done: true } for that call. The synchronous adapter does not persist natural completion: a later next() invokes Rust again. If later calls must remain done, record that state in your struct and keep returning None.

return() and complete

Override complete for cleanup when JavaScript closes iteration early—for example, when a for...of loop executes break.

lib.rs
rust
fn complete(&mut self, _value: Option<Self::Return>) -> Option<Self::Yield> {
  self.release_native_cursor();
  None
}

The synchronous adapter currently invokes complete, marks the generator done, and uses the argument supplied by JavaScript as the returned iterator-result value. The Option<Yield> returned by complete is not currently exposed. Treat it as a cleanup hook and do not rely on its return value until this experimental API is stabilized.

throw() and catch

The default catch returns the original JavaScript value as Err, so iterator.throw(error) throws that value and completes the iterator.

Override it to recover:

lib.rs
rust
fn catch<'env>(
  &'env mut self,
  _env: Env,
  value: Unknown<'env>,
) -> std::result::Result<Option<Self::Yield>, Unknown<'env>> {
  if self.can_recover() {
    Ok(Some(self.fallback()))
  } else {
    Err(value)
  }
}
  • Err(value) throws value and completes iteration.
  • Ok(Some(value)) yields it with done: false.
  • Ok(None) completes without throwing.

Use std::result::Result in the signature above because the error side is the original Unknown, not napi::Error.

Scoped synchronous yields

Generator::Yield must be an owned or otherwise directly convertible value. Implement ScopedGenerator<'env> when a yielded value borrows the current JavaScript environment:

lib.rs
rust
use napi::iterator::ScopedGenerator;

#[napi(iterator)]
pub struct ObjectCounter {
  current: u32,
  end: u32,
}

#[napi]
impl<'env> ScopedGenerator<'env> for ObjectCounter {
  type Yield = Object<'env>;
  type Next = ();
  type Return = ();

  fn next(
    &mut self,
    env: &'env Env,
    _value: Option<Self::Next>,
  ) -> Option<Self::Yield> {
    if self.current >= self.end {
      return None;
    }
    let mut object = Object::new(env).ok()?;
    object.set("value", self.current).ok()?;
    self.current += 1;
    Some(object)
  }
}

The scoped trait receives &Env in next and catch. The yielded value is converted immediately on the JavaScript thread; it must not be stored in the class or moved to another thread.

Iterator helpers and prototypes

Symbol.iterator returns the class instance itself. On runtimes that expose the global Iterator constructor, napi-rs adjusts the generated class prototype to inherit from Iterator.prototype, which makes iterator-helper methods such as map, filter, take, and drop available. On runtimes without that global, the basic iteration protocol still works but those helpers are absent.

Because prototype integration is part of this experimental API, test subclassing and any code that freezes or replaces class prototypes.

Async iterator

Enable the async runtime:

Cargo.toml
toml
[dependencies]
napi = { version = "3", features = ["async", "tokio_time"] }
napi-derive = "3"

Then mark the class and implement AsyncGenerator:

lib.rs
rust
use std::future::Future;

use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi(async_iterator)]
pub struct DelayedCounter {
  current: u32,
  end: u32,
  delay_ms: u64,
}

#[napi]
impl AsyncGenerator for DelayedCounter {
  type Yield = u32;
  type Next = ();
  type Return = ();

  fn next(
    &mut self,
    _value: Option<Self::Next>,
  ) -> impl Future<Output = Result<Option<Self::Yield>>> + Send + 'static {
    let value = self.current;
    let end = self.end;
    let delay_ms = self.delay_ms;
    self.current += 1;

    async move {
      napi::tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
      Ok((value < end).then_some(value))
    }
  }
}

#[napi]
impl DelayedCounter {
  #[napi(constructor)]
  pub fn new(end: u32, delay_ms: u32) -> Self {
    Self { current: 0, end, delay_ms: delay_ms as u64 }
  }
}
index.mjs
js
for await (const value of new DelayedCounter(3, 10)) {
  console.log(value) // 0, 1, 2
}

The generated class implements:

ts
[Symbol.asyncIterator](): AsyncGenerator<Yield, Return, Next | undefined>

Async bounds

AsyncGenerator deliberately prevents JavaScript-scoped values from crossing an await point:

rust
type Yield: ToNapiValue + Send + 'static;

fn next(
  &mut self,
  value: Option<Self::Next>,
) -> impl Future<Output = Result<Option<Self::Yield>>> + Send + 'static;

The future cannot borrow self. Update synchronous state and copy or clone everything the future needs before creating the async move block, as the example does. A scoped Object<'env>, Function<'env, ...>, or BufferSlice<'env> cannot be the yielded type. Return owned values, or create a JavaScript value later in a different API that explicitly supplies Env on the JavaScript thread.

Async next()

Each call returns a Promise:

  • Ok(Some(value)) fulfills with { value, done: false }.
  • Ok(None) fulfills with { value: undefined, done: true }.
  • Err(error) rejects the Promise.

The current experimental async adapter does not keep a separate terminal-state flag after Ok(None). If a subsequent call must remain completed, keep that state in your Rust struct and continue returning Ok(None).

Do not assume overlapping next() calls are serialized for you. State mutation before the future is returned happens immediately on the JavaScript thread, while the resulting futures can remain in flight together. Design the state machine for concurrent in-flight operations or document that callers must await one result before requesting the next.

Async return()

Override complete to perform asynchronous cleanup:

lib.rs
rust
fn complete(
  &mut self,
  _value: Option<Self::Return>,
) -> impl Future<Output = Result<Option<Self::Yield>>> + Send + 'static {
  let handle = self.take_handle();
  async move {
    handle.close().await.map_err(Error::from)?;
    Ok(None)
  }
}

The returned Promise always resolves with done: true; Some(value) becomes its final value and None becomes undefined. An error rejects. As with next, the adapter does not itself persist a terminal flag for later operations, so record completion in your class if callers can retain and reuse the iterator object.

There is an experimental typing mismatch here: complete returns Option<Self::Yield> at runtime, while the generated AsyncGenerator<Yield, Return, Next> declaration types the final value as Return. If complete can return Some(value), keep Yield and Return the same type; otherwise return None. With different Yield and Return types, the generated declaration can disagree with the runtime value.

JavaScript normally calls return() when a for await...of loop exits early, but cleanup should still tolerate the iterator being garbage-collected without an orderly return.

Async throw()

The default catch turns the thrown JavaScript value into napi::Error, so the returned Promise rejects.

lib.rs
rust
fn catch(
  &mut self,
  _env: Env,
  value: Unknown,
) -> impl Future<Output = Result<Option<Self::Yield>>> + Send + 'static {
  let error: Error = value.into();
  async move { Err(error) }
}

A custom catch can recover with Ok(Some(value)). In the current experimental adapter, a recovered Ok(None) is represented as a non-terminal result with a null value; use Err to rethrow or Ok(Some(...)) to recover, and use return()/explicit class state to model completion.

See Error handling for JavaScript error preservation across async work.

Lifetime and garbage collection

For async iteration, [Symbol.asyncIterator]() creates an iterator object that keeps a hidden, non-enumerable, non-writable reference to the native class instance. This prevents the class from being collected while the iterator is retained. The reference is released when the iterator object is finalized.

That reference does not cancel an in-flight Rust future. Futures and any external resources they own need their own cancellation and shutdown design. Keep cleanup idempotent so it is safe from complete, explicit class methods, and Drop/finalization paths.

The synchronous iterator is the class instance itself, so normal class-instance reachability keeps the Rust value alive.

Choosing another abstraction

Use an iterator when each request naturally produces one item and the consumer controls pacing.

  • Use a normal array or Vec<T> for small, already-materialized results.
  • Use ReadableStream for streaming with Web Streams backpressure and cancellation semantics.
  • Use AsyncTask for one CPU-heavy result from libuv's worker pool.
  • Use an async function for one Tokio future and one Promise.
  • Use ThreadsafeFunction for repeated callbacks originating on a native thread.

Because iterator support is experimental, prefer these established abstractions when interoperability or long-term API stability matters more than the iterator syntax.

Test checklist

  • next() with and without its argument.
  • Natural completion and calls after completion.
  • Early break, explicit return(value), and cleanup failure.
  • Default and recovered throw(error) behavior.
  • Two overlapping async next() calls if the API permits them.
  • Dropping the original async class while retaining only its iterator.
  • Forced garbage collection and worker-environment shutdown.
  • Runtimes both with and without the global Iterator helper API.