DEV Community

HarmonyOS
HarmonyOS

Posted on

Node API Part-4 : Calling an ArkTS Method with Return Value of a Promise Using Node-API

Read the original article:Node API Part-4 : Calling an ArkTS Method with Return Value of a Promise Using Node-API

Introduction:

As HarmonyOS development evolves, the need to bridge high-performance native code with the expressive power of ArkTS becomes more important than ever. Whether you’re building for system-level features or enhancing app responsiveness, there often comes a time when your native C++ module needs to call an ArkTS method that returns a Promise.

So how do you manage this interaction smoothly — especially when that method is asynchronous?

🧩 The Problem Space

When ArkTS returns a Promise, you’re essentially getting a future value — something that will resolve (or reject) later. Native C++ code, however, expects immediate feedback and deterministic control flow. This mismatch can lead to tricky situations, especially in multithreaded or performance-critical environments.

The goal, then, is to call the ArkTS function from C++ in a way that:

  • Doesn’t block the native thread,
  • Handles the Promise’s resolution or rejection cleanly, and
  • Converts the result into something usable on the C++ side.

🛡️ Why It Matters

This approach is more than just a clever workaround. It enables a clean, maintainable bridge between two very different runtime worlds. ArkTS can handle UI logic, timers, or network calls with its async-first architecture, while C++ can handle low-level tasks and deliver performance where it counts.

By connecting the two correctly:

  • You keep your native threads unblocked.
  • You avoid memory leaks or race conditions.
  • You maintain HarmonyOS’s reactive and efficient architecture.

💡 When to Use It

You might use this technique when:

  • You need to call ArkTS utility functions (like timers or API wrappers) - from a C++ background module.
  • You’re integrating native modules into the ArkTS runtime for flexibility.
  • You’re building hybrid applications with shared logic.

🚀 The Integration Flow

The general strategy is simple in concept but powerful in execution:

  1. Call the ArkTS Function: From C++, you invoke the target ArkTS method (passed in as a function), which returns a Promise.

  2. Attach Native Callbacks: You bind native handlers to the Promise’s .then() call — one for successful resolution and one for rejection.

  3. Handle the Result Safely: Whether the Promise resolves with data or fails with an error, your C++ callbacks receive the result, convert it if needed, and proceed accordingly.

What’s especially elegant here is how the system preserves asynchronicity and thread safety. You’re not hacking your way around Promise internals — you’re integrating using proper Node-API mechanisms.

⚙️ Implementation in Sample Project

🧩 API Declaration in index.d.ts

export const callArkTSAsync: (func: Function) => object;
Enter fullscreen mode Exit fullscreen mode

index.d.ts

🏗️napi_init.cpp

#include "hilog/log.h"
#include "napi/native_api.h"
#include <napi/common.h>
#include <pthread.h>
#include <hilog/log.h>

// Callback for handling a resolved Promise from ArkTS.
static napi_value ResolvedCallback(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value args[1];
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    int result;
    napi_get_value_int32(env, args[0], &result);

    // Log the resolved result from the Promise.
    OH_LOG_INFO(LOG_APP, "Promise resolved with result:%{public}d", result);

    return nullptr;
}

// Callback for handling a rejected Promise from ArkTS.
static napi_value RejectedCallback(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value args[1];
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    napi_value error;
    // Convert the error value to a string for logging.
    napi_coerce_to_string(env, args[0], &error);
    char errorMsg[1024];
    size_t len;
    napi_get_value_string_utf8(env, error, errorMsg, sizeof(errorMsg), &len);

    // Log the rejection reason from the Promise.
    OH_LOG_ERROR(LOG_APP, "Promise rejected with error:%{public}s", errorMsg);

    return nullptr;
}

// Function to call an ArkTS method that returns a Promise and attach callbacks.
static napi_value CallArkTSAsync(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value argv[1] = {nullptr};
    napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);

    napi_value promise = nullptr;
    // Call the passed ArkTS function to obtain the Promise.
    napi_call_function(env, nullptr, argv[0], 0, nullptr, &promise);

    napi_value thenFunc = nullptr;
    // Retrieve the 'then' method from the Promise.
    if (napi_get_named_property(env, promise, "then", &thenFunc) != napi_ok) {
        return nullptr;
    }

    napi_value onResolve = nullptr;
    napi_value onReject = nullptr;
    // Create native C++ callbacks for handling resolution and rejection.
    napi_create_function(env, "onResolve", NAPI_AUTO_LENGTH, ResolvedCallback, nullptr, &onResolve);
    napi_create_function(env, "onReject", NAPI_AUTO_LENGTH, RejectedCallback, nullptr, &onReject);

    napi_value argv1[2] = {onResolve, onReject};
    // Attach the callbacks to the Promise.
    napi_call_function(env, promise, thenFunc, 2, argv1, nullptr);

    return nullptr;
}

// Register the native method to be accessible from JavaScript.
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
    napi_property_descriptor desc[] = {
        {"callArkTSAsync", nullptr, CallArkTSAsync, nullptr, nullptr, nullptr, napi_default, nullptr}};
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END

// Module definition required by Node-API.
static napi_module nativeModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "entry",
    .nm_priv = nullptr,
    .reserved = {0},
};

// Register the native module when the shared library is loaded.
extern "C" __attribute__((constructor)) void RegisterEntryModule() {
    napi_module_register(&nativeModule);
}
Enter fullscreen mode Exit fullscreen mode

napi_init.cpp

📲ObjectUtil.ts

import testNapi from 'libentry.so'

export function SetTimeout(): Promise<number> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(42);
    }, 1000)
  })
}

export function ClickMethod() {
  testNapi.callArkTSAsync(SetTimeout);
}
Enter fullscreen mode Exit fullscreen mode

ObjectUtil.ts

▶️Index.ets

import { ClickMethod } from './ObjectUtils';

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            ClickMethod()
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}
Enter fullscreen mode Exit fullscreen mode

Index.ets

🧮build-profile.json5 (Entry Level)

Set runtimeOnly config in arkOptions-buildOption please.

{
  "apiType": "stageMode",
  "buildOption": {
    "arkOptions": {
      "runtimeOnly": {
        "sources": [
          "./src/main/ets/pages/ObjectUtils.ets"
        ]
      }
    },
    "externalNativeOptions": {
      "path": "./src/main/cpp/CMakeLists.txt",
      "arguments": "",
      "cppFlags": "",
      "abiFilters": [
        "arm64-v8a",
        "x86_64"
      ]
    }
  },
  "buildOptionSet": [
    {
      "name": "release",
      "arkOptions": {
        "obfuscation": {
          "ruleOptions": {
            "enable": false,
            "files": [
              "./obfuscation-rules.txt"
            ]
          }
        }
      },
      "nativeLib": {
        "debugSymbol": {
          "strip": true,
          "exclude": []
        }
      }
    },
  ],
  "targets": [
    {
      "name": "default"
    },
    {
      "name": "ohosTest",
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

build-profile.json5

🎯 Understanding runtimeOnly in HarmonyOS Stage Model

In HarmonyOS Stage Model applications, the runtimeOnly configuration plays a key role in controlling when and how specific .ets (ArkTS) files are bundled and loaded. This guide will help you understand what it does, when to use it, and why it matters.

✅ What is runtimeOnly?

  • The runtimeOnly option is part of the arkOptions configuration in the build-profile.json5 or similar build file.
  • It specifies .ets source files that should not be bundled into the app during initial build.
  • These files are excluded from the main app package and are instead loaded dynamically at runtime.

💡 Why Use runtimeOnly?

Here are the primary reasons for using runtimeOnly:

  • 🔄 Reduces App Startup Time
    Files not immediately needed are not bundled in the initial package, improving startup performance.

  • 📦 Decreases Package Size
    Minimizes the size of the base APK by deferring optional modules.

  • ⚙️ Enables On-Demand Loading
    Perfect for scenarios where certain features or pages are used rarely or conditionally.

  • 🧪 Supports Modularization
    Helps you separate optional features into loosely coupled modules for better maintainability.

⚠️ Important Notes

  • If a runtimeOnly file is statically imported by another .ets file, it will be included in the final package, and the runtimeOnly directive will be ignored.
  • Use dynamic loading techniques to fully benefit from this setting.
  • ✅ Use runtimeOnly for rarely used features, such as help pages, onboarding screens, or admin-only tools.
  • ✅ Combine with dynamic import logic to fetch the module only when needed.
  • ❌ Avoid marking frequently used core modules as runtimeOnly, as it may degrade runtime performance due to delayed loading.

🧾The runtimeOnly option is a powerful tool in HarmonyOS for:

  • Improving startup performance
  • Optimizing package size
  • Supporting feature modularization
  • Enabling dynamic ArkTS module loading

✨ Conclusion

Integrating Promise-based ArkTS methods with C++ via Node-API might sound complex, but once the groundwork is in place, it becomes a reliable tool in your development toolkit. It’s a perfect example of HarmonyOS’s flexibility — letting you build fast, responsive, and modern applications by using the right tool for each layer of the system.

If you’re building serious apps on HarmonyOS and haven’t explored this bridge yet, now might be the perfect time.

📚 Additional Resources

Document

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

📘 Node-API for HarmonyOS

📙 ArkTS Async Programming Guide1

https://forums.developer.huawei.com/forumPortal/en/topic/0203190042005614066?fid=0102647487706140266

Written by Bunyamin Eymen Alagoz

Top comments (0)