DEV Community

HarmonyOS
HarmonyOS

Posted on

Node-API Part-5 : Asynchronous Task Development Using Node-API

Read the original article:Node-API Part-5 : Asynchronous Task Development Using Node-API

🚀Node-API Part-5 : Asynchronous Task Development Using Node-API

✨ “What happens when the power of native C++ meets the async elegance of ArkTS?” Let’s find out, and make them handshake over a Promise. 🤝

🧭 Introduction

When developing native modules using Node-API for HarmonyOS, maintaining application responsiveness is essential — especially for time-consuming operations like file I/O, image processing, or network communication. Blocking the main thread can lead to performance bottlenecks and poor user experience. Fortunately, napi_create_async_work provides an elegant way to offload heavy tasks to a worker thread without impacting the UI or logic layers of your application.

This article walks you through using asynchronous tasks in Node-API with both Promise-based and callback-based implementations. By the end, you’ll have a practical understanding of when and how to use napi_async_work in your HarmonyOS native modules.

🔍 Context: Why Call ArkTS Promises from Native Code?

You might want this if:

  • Your native C++ module needs to execute ArkTS logic.
  • That ArkTS logic involves asynchronous operations (e.g. setTimeout, network requests).
  • You want to handle .then() and .catch() callbacks from within native C++.

🧠 Think: native modules reacting to async ArkTS behaviors — without breaking flow.

📌 When to Use napi_create_async_work

Use napi_create_async_work when your native code performs tasks that are:

  • File-heavy (e.g., reading large files)
  • Network-bound (e.g., making API requests)
  • Database-intensive (e.g., complex queries or inserts)
  • Image-processing related (e.g., resizing, filtering)

These operations should not run on the main thread, as they may freeze the UI or delay logic execution. Instead, you can offload them using asynchronous work queues provided by Node-API.

⚙️ Implementation

📁 API Declaration in index.d.ts


export const asyncWorkPromiseSample: (data: number) => Promise<number>;

export const asyncWorkCallbacksSample: (arg1: number, arg2: number, callback: (result: number) => void) => void;

Enter fullscreen mode Exit fullscreen mode

🔧 Promise Structs

promise_sample.h


#ifndef NDKBASICS_PROMISE_SAMPLE_H
#define NDKBASICS_PROMISE_SAMPLE_H

#include "napi/native_api.h"

class promise_sample {

};

napi_value asyncWorkPromiseSample(napi_env env, napi_callback_info info);

#endif 


Enter fullscreen mode Exit fullscreen mode

promise_sample.cpp

#include "promise_sample.h"

// Structure to hold callback-related data for async work
struct CallbackData {
    napi_async_work asyncWork = nullptr;  // Handle to async work
    napi_deferred deferred = nullptr;     // Deferred object for promise
    napi_ref callback = nullptr;          // (Unused here) Reference to a JS callback function
    double args = 0;                      // Input argument
    double result = 0;                    // Result to return
};

// This function runs in a separate thread and performs the actual computation
static void ExecuteCB(napi_env env, void *data)
{
    auto *callbackData = reinterpret_cast<CallbackData *>(data);
    // In this example, we just pass the input value as the result
    callbackData->result = callbackData->args;
}

// This function runs in the main thread once the async work is complete
static void CompleteCB(napi_env env, napi_status status, void *data)
{
    auto *callbackData = reinterpret_cast<CallbackData *>(data);
    napi_value result = nullptr;
    napi_create_double(env, callbackData->result, &result);

    // Resolve or reject the promise based on the result value
    if (callbackData->result > 0) {
        napi_resolve_deferred(env, callbackData->deferred, result);
    } else {
        napi_reject_deferred(env, callbackData->deferred, result);
    }

    // Clean up the async work and free allocated memory
    napi_delete_async_work(env, callbackData->asyncWork);
    delete callbackData;
}

// This is the native function exposed to JavaScript
// It starts an asynchronous operation and returns a Promise
napi_value asyncWorkPromiseSample(napi_env env, napi_callback_info info)
{
    size_t argc = 1;
    napi_value args[1];

    // Get the arguments passed from JavaScript
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    napi_value promise = nullptr;
    napi_deferred deferred = nullptr;

    // Create a new promise and get its deferred handle
    napi_create_promise(env, &deferred, &promise);

    // Allocate and initialize callback data
    auto *callbackData = new CallbackData();
    callbackData->deferred = deferred;
    napi_get_value_double(env, args[0], &callbackData->args);

    // Create a string to name the async work
    napi_value resourceName = nullptr;
    napi_create_string_utf8(env, "AsyncCallback", NAPI_AUTO_LENGTH, &resourceName);

    // Create the async work item
    napi_create_async_work(env, nullptr, resourceName, ExecuteCB, CompleteCB, callbackData, &callbackData->asyncWork);

    // Queue the async work to be executed
    napi_queue_async_work(env, callbackData->asyncWork);

    // Return the promise to JavaScript
    return promise;
}

Enter fullscreen mode Exit fullscreen mode

🔧 Callback Structs

callbacks_sample.h


#ifndef NDKBASICS_CALLBACKS_SAMPLE_H
#define NDKBASICS_CALLBACKS_SAMPLE_H

#include "napi/native_api.h"

class callbacks_sample {

};

napi_value asyncWorkCallbacksSample(napi_env env, napi_callback_info info);

#endif 

Enter fullscreen mode Exit fullscreen mode

callbacks_sample.h


#ifndef NDKBASICS_CALLBACKS_SAMPLE_H
#define NDKBASICS_CALLBACKS_SAMPLE_H

#include "napi/native_api.h"

class callbacks_sample {

};

napi_value asyncWorkCallbacksSample(napi_env env, napi_callback_info info);

#endif 

Enter fullscreen mode Exit fullscreen mode

callbacks_sample.cpp

#include "callbacks_sample.h"
#include <js_native_api.h>
#include <node_api.h>
#include <node_api_types.h>

// Struct to hold data required across async work lifecycle
struct CallbackData {
  napi_async_work asyncWork = nullptr;     // Handle for the async work
  napi_ref callbackRef = nullptr;          // Reference to the JS callback function
  double args[2] = {0};                    // Input arguments
  double result = 0;                       // Computed result (sum of args)
};

// Function executed on a worker thread (non-blocking)
// Performs the actual computation (sum of two numbers)
static void ExecuteCB(napi_env env, void *data)
{
    CallbackData *callbackData = reinterpret_cast<CallbackData *>(data);
    callbackData->result = callbackData->args[0] + callbackData->args[1];
}

// Function executed on the main thread after async work completes
// Calls the stored JS callback with the result
static void CompleteCB(napi_env env, napi_status status, void *data)
{
    CallbackData *callbackData = reinterpret_cast<CallbackData *>(data);

    // Create a JavaScript number to hold the result
    napi_value callbackArg[1] = {nullptr};
    napi_create_double(env, callbackData->result, &callbackArg[0]);

    // Retrieve the original JS callback function from reference
    napi_value callback = nullptr;
    napi_get_reference_value(env, callbackData->callbackRef, &callback);

    // Call the callback with the result
    napi_value result;
    napi_value undefined;
    napi_get_undefined(env, &undefined);
    napi_call_function(env, undefined, callback, 1, callbackArg, &result);

    // Clean up resources: reference and async work
    napi_delete_reference(env, callbackData->callbackRef);
    napi_delete_async_work(env, callbackData->asyncWork);
    delete callbackData;
}

// This function is exposed to JavaScript
// It performs asynchronous computation and calls a callback upon completion
napi_value asyncWorkCallbacksSample(napi_env env, napi_callback_info info)
{
    size_t argc = 3;
    napi_value args[3];

    // Retrieve arguments passed from JavaScript
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    // Create and initialize callback data to store across async lifecycle
    auto asyncContext = new CallbackData();
    napi_get_value_double(env, args[0], &asyncContext->args[0]);  // First number
    napi_get_value_double(env, args[1], &asyncContext->args[1]);  // Second number

    // Convert the JS callback function to a persistent reference
    napi_create_reference(env, args[2], 1, &asyncContext->callbackRef);

    // Create a string resource name for async work (for debugging/profiling)
    napi_value resourceName = nullptr;
    napi_create_string_utf8(env, "asyncWorkCallback", NAPI_AUTO_LENGTH, &resourceName);

    // Create the async work object with execution and completion callbacks
    napi_create_async_work(env, nullptr, resourceName, ExecuteCB, CompleteCB,
                           asyncContext, &asyncContext->asyncWork);

    // Queue the async work for execution
    napi_queue_async_work(env, asyncContext->asyncWork);

    // Return undefined (no immediate return value, result will be sent via callback)
    return nullptr;
}

Enter fullscreen mode Exit fullscreen mode

napi_init.cpp

#include "callbacks/callbacks_sample.h"
#include "napi/native_api.h"
#include "promise/promise_sample.h"

EXTERN_C_START  // Ensures C-style linkage for compatibility with Node.js

// This function is called when the module is initialized
static napi_value Init(napi_env env, napi_value exports) {

    // Define properties (functions) to be exported to JavaScript
    napi_property_descriptor desc[] = {
        {
            "asyncWorkPromiseSample",  // JavaScript function name
            nullptr,
            asyncWorkPromiseSample,    // Native function for Promise-based async work
            nullptr, nullptr, nullptr,
            napi_default,
            nullptr
        },
        {
            "asyncWorkCallbacksSample",  // JavaScript function name
            nullptr,
            asyncWorkCallbacksSample,    // Native function for callback-based async work
            nullptr, nullptr, nullptr,
            napi_default,
            nullptr
        }
    };

    // Bind the functions to the exports object
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);

    // Return the updated exports object
    return exports;
}

EXTERN_C_END  // End of C-style linkage block

// Define the module descriptor for registration
static napi_module demoModule = {
    .nm_version = 1,                  // N-API version
    .nm_flags = 0,                    // Reserved (set to 0)
    .nm_filename = nullptr,          // Optional: filename (unused here)
    .nm_register_func = Init,        // Module initialization function
    .nm_modname = "entry",           // Module name used in JS: require('entry')
    .nm_priv = ((void *)0),          // Private data (not used)
    .reserved = {0},                 // Reserved fields
};

// Register the native module when the shared object is loaded
extern "C" __attribute__((constructor)) void RegisterEntryModule(void) {
    napi_module_register(&demoModule);
}

Enter fullscreen mode Exit fullscreen mode

Modular Initialization with Init()

The Init function serves as the entry point when the native module is loaded. It binds two native functions—asyncWorkPromiseSample and asyncWorkCallbacksSample—to JavaScript-accessible properties using napi_property_descriptor. This allows JavaScript code to call native functions using familiar names.

Promise & Callback-Based Async APIs

Two different asynchronous patterns are supported:

  • asyncWorkPromiseSample returns a Promise for async control in modern JavaScript.
  • asyncWorkCallbacksSample supports traditional callback-style async handling.

This makes the native module flexible for different usage patterns in ArkTS or Node.js-style environments.

C Compatibility with EXTERN_C_START and EXTERN_C_END

hese macros wrap the native functions to ensure C-style linkage, which prevents name mangling and ensures compatibility with Node-API expectations, especially when the code is compiled as C++.

Self-Registering Module with napi_module_register

The module defines a static napi_module structure, specifying metadata such as:

  • The module name (entry)
  • The init function (Init)
  • API version and flags

Then, using the special GCC constructor attribute, the RegisterEntryModule() function is automatically called when the shared library is loaded—no manual registration needed.

Exports Ready for JavaScript Integration

Once compiled and linked, the resulting .so (shared object) file exposes two functions directly usable from ArkTS or JavaScript:

import { asyncWorkPromiseSample, asyncWorkCallbacksSample } from 'libentry.so';

Configuration

CMakeLists.txt

# the minimum version of CMake.
cmake_minimum_required(VERSION 3.5.0)
project(NDKBasics)

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})

if(DEFINED PACKAGE_FIND_FILE)
    include(${PACKAGE_FIND_FILE})
endif()

include_directories(${NATIVERENDER_ROOT_PATH}
                    ${NATIVERENDER_ROOT_PATH}/include)

add_library(entry SHARED napi_init.cpp promise/promise_sample.cpp callbacks/callbacks_sample.cpp)

target_link_libraries(entry PUBLIC libace_napi.z.so)
Enter fullscreen mode Exit fullscreen mode

🧩 Define ArkTS Script (Index.ets)

📈 Visual Architecture

ArkTS Promise (async logic)
|
v
C++ Calls ArkTS (via Node-API)
|
v
Handles .then() using:
├── ✅ ResolvedCallback
└── ❌ RejectedCallback
🖱️ Click → ArkTS Method → Native C++ Callbacks → Logs 🪵

🧠 Key Takeaways

✅ Use napi_call_function() to invoke ArkTS methods.

✅ Always attach both .then() and .catch() for full Promise handling.

✅ Convert JS values using napi_get_value_* before logging or processing.

✅ Use hilog for safe, structured native logs.

👋 Conclusion

Bridging native and ArkTS async logic opens powerful integration paths. Whether you’re offloading work to C++ or responding to UI triggers in ArkTS, managing Promises from native land is a game-changer.

And now, you’re ready to build that bridge — brick by brick, API by API

🧱🌉. Happy coding on HarmonyOSNext! 💙

📚 Additional Resources

Document

The OpenCms demo, brought to you by Alkacon Software.developer.huawei.com

Written by Bunyamin Eymen Alagoz

Top comments (0)