NAPI
async functions
Promises
Node.js
JavaScript development

How to create async function using NAPI that return Promises

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Node-API lets native addons expose asynchronous operations without blocking the event loop. If you want the JavaScript side to use await, the standard pattern is to create a promise, schedule async work, and resolve or reject that promise in the completion callback.

The Core Node-API Pattern

An async promise-returning function usually needs these steps:

  • create a promise and keep the deferred handle
  • create an async work item
  • do the heavy work in the execute callback
  • resolve or reject the deferred promise in the complete callback
  • clean up the work item and native data

The central APIs are napi_create_promise, napi_create_async_work, napi_queue_async_work, napi_resolve_deferred, and napi_reject_deferred.

Complete Example

The example below exposes doAsyncWork(value) and returns a JavaScript promise:

cpp
1#include <node_api.h>
2#include <assert.h>
3#include <string>
4
5struct AsyncData {
6  napi_async_work work;
7  napi_deferred deferred;
8  int32_t input;
9  int32_t result;
10  std::string error;
11};
12
13static void Execute(napi_env env, void* data) {
14  AsyncData* asyncData = static_cast<AsyncData*>(data);
15
16  if (asyncData->input < 0) {
17    asyncData->error = "input must be non-negative";
18    return;
19  }
20
21  asyncData->result = asyncData->input * 2;
22}
23
24static void Complete(napi_env env, napi_status status, void* data) {
25  AsyncData* asyncData = static_cast<AsyncData*>(data);
26  napi_value value;
27
28  if (status != napi_ok || !asyncData->error.empty()) {
29    napi_value message;
30    napi_create_string_utf8(env,
31                            asyncData->error.empty() ? "async work failed" : asyncData->error.c_str(),
32                            NAPI_AUTO_LENGTH,
33                            &message);
34    napi_value error;
35    napi_create_error(env, nullptr, message, &error);
36    napi_reject_deferred(env, asyncData->deferred, error);
37  } else {
38    napi_create_int32(env, asyncData->result, &value);
39    napi_resolve_deferred(env, asyncData->deferred, value);
40  }
41
42  napi_delete_async_work(env, asyncData->work);
43  delete asyncData;
44}
45
46static napi_value DoAsyncWork(napi_env env, napi_callback_info info) {
47  size_t argc = 1;
48  napi_value args[1];
49  napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
50
51  AsyncData* asyncData = new AsyncData();
52  napi_get_value_int32(env, args[0], &asyncData->input);
53
54  napi_value promise;
55  napi_create_promise(env, &asyncData->deferred, &promise);
56
57  napi_value resourceName;
58  napi_create_string_utf8(env, "DoAsyncWork", NAPI_AUTO_LENGTH, &resourceName);
59
60  napi_create_async_work(
61      env,
62      nullptr,
63      resourceName,
64      Execute,
65      Complete,
66      asyncData,
67      &asyncData->work);
68
69  napi_queue_async_work(env, asyncData->work);
70  return promise;
71}
72
73static napi_value Init(napi_env env, napi_value exports) {
74  napi_value fn;
75  napi_create_function(env, nullptr, 0, DoAsyncWork, nullptr, &fn);
76  napi_set_named_property(env, exports, "doAsyncWork", fn);
77  return exports;
78}
79
80NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

On the JavaScript side:

javascript
1const addon = require("./build/Release/addon");
2
3async function main() {
4  const value = await addon.doAsyncWork(21);
5  console.log(value);
6}
7
8main().catch(console.error);

Minimal binding.gyp

To build the addon with node-gyp, keep the build config simple:

json
1{
2  "targets": [
3    {
4      "target_name": "addon",
5      "sources": ["addon.cpp"]
6    }
7  ]
8}

Then run:

bash
node-gyp configure
node-gyp build

Error Handling Matters

There are two different failure paths:

  • Node-API call fails immediately
  • async work runs but produces an application error

The example handles the second path by storing an error string in AsyncData and rejecting the promise in Complete. In real code, you should also check the return status of each Node-API call and bail out early if napi_ok is not returned.

Keep Heavy Work Out of the Completion Callback

The execute callback runs on a worker thread. The complete callback runs back on the main JavaScript thread. That means:

  • CPU-heavy native work belongs in Execute
  • JavaScript value creation and promise resolution belong in Complete

Mixing those responsibilities is a common source of crashes or event-loop stalls.

Common Pitfalls

  • Returning a promise but forgetting to store the napi_deferred handle needed to resolve it later.
  • Creating JavaScript values in the worker-thread execute callback.
  • Forgetting to delete the async work item and native context after completion.
  • Blocking inside the main-thread completion callback and defeating the purpose of async work.
  • Skipping Node-API status checks and making failures much harder to diagnose.

Summary

  • Create a promise first with napi_create_promise.
  • Schedule background work with napi_create_async_work and napi_queue_async_work.
  • Do native computation in the execute callback, not on the main thread.
  • Resolve or reject the deferred promise in the completion callback.
  • Always clean up the async work handle and any native context you allocated.

Course illustration
Course illustration

All Rights Reserved.