skip to content

In an Expo module's ModuleDefinition, how do Constant, Property, Function and AsyncFunction differ in what JavaScript sees and where they run?

level: middleimportance: should knowfreq 35%

answer

  1. value, accessor, sync call, Promise
  2. Constant is computed once
  3. Property runs its getter each read
  4. Function blocks the JavaScript thread
  5. runOnQueue for main-thread work

basics

~20 s

Constant exposes a value computed once and cached; Property runs its getter, and optional setter, on each access; Function is synchronous and blocks JavaScript; AsyncFunction returns a Promise and runs off the JavaScript thread by default.

solid answer

~40 s

All four become members of the module object that `requireNativeModule` returns. `Constant("name") { ... }` is computed on first access and cached, so it suits values that never change, like a maximum pattern length. `Property` works like `Object.defineProperty`: its getter runs on every read, and `.set` adds a setter. `Function` is synchronous: the native closure runs on the JavaScript thread and blocks the script until it returns, so it is for cheap work. `AsyncFunction` always returns a `Promise`; by default its native body is dispatched off the JavaScript thread, and `.runOnQueue(.main)` on iOS or `Queues.MAIN` on Android moves it to the main thread for UI-affine APIs. Throwing inside an `AsyncFunction` rejects the Promise. Each function takes up to eight arguments.

code

kotlin · 34 lines
kotlin
package expo.modules.hapticpatterns

import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition

class HapticPatternsModule : Module() {
  @Suppress("DEPRECATION")
  private val vibrator: Vibrator
    get() = requireNotNull(appContext.reactContext)
      .getSystemService(Context.VIBRATOR_SERVICE) as Vibrator

  override fun definition() = ModuleDefinition {
    Name("HapticPatterns")

    Constant("maxSteps") { 32 }

    Property("isAvailable") { vibrator.hasVibrator() }

    Function("patternLength") { timingsMs: LongArray ->
      timingsMs.sum()
    }

    AsyncFunction("playPatternAsync") { timingsMs: LongArray ->
      if (timingsMs.size > 32) throw IllegalArgumentException("Too many steps")
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        vibrator.vibrate(VibrationEffect.createWaveform(timingsMs, -1))
      }
    }
  }
}

go deeper

for a junior

Recall the four components: a cached constant, a property with a getter, a synchronous function and an asynchronous function returning a Promise.

for a middle

Explain when each runs and on which thread, why Function blocks JavaScript, and how runOnQueue moves an AsyncFunction to the main thread.

for a senior

Justify the member type for each part of a real API, such as a haptics module, including main-thread-bound platform calls and error propagation through rejected Promises.

for a principal

Set conventions for module APIs across a team: async by default for anything touching I/O or the UI, synchronous calls only for cheap pure work, and consistent error shapes.

## One object, four kinds of member An Expo module's native class returns a **`ModuleDefinition`** built from DSL components. JavaScript loads the module with `requireNativeModule('Name')` (from `expo`) and gets a single object; each component adds a member to it: | component | JavaScript sees | when native code runs | thread | |---|---|---|---| | `Constant("x") { ... }` | a read-only value | once, on first access; cached after | the JavaScript thread | | `Property("x") { ... }` | an accessor property | on every read (and write with `.set`) | the JavaScript thread | | `Function("f") { ... }` | a synchronous function | on each call; blocks JavaScript | the JavaScript thread | | `AsyncFunction("f") { ... }` | a function returning a `Promise` | on each call | a background queue by default | ## Constant and Property **`Constant`** replaces the older `Constants` component, which is now deprecated. The closure runs the first time JavaScript reads the value and the result is cached, so it is right for facts that cannot change while the app runs, such as a maximum number of steps in a haptic pattern. **`Property`** is the same as defining a property with `Object.defineProperty` on the module object: - the short form `Property("isAvailable") { ... }` is **read-only**, and its getter runs **on every read**; - the long form `Property("volume").get { ... }.set { newValue in ... }` adds a **setter**, so `Module.volume = 0.5` in JavaScript calls native code. Use a property when the value can change, such as whether haptics are currently enabled in system settings; a constant would freeze the first answer. ## Function: synchronous and blocking **`Function`** exports a synchronous native function. When JavaScript calls it, the native closure runs on the **JavaScript thread** and the script waits for the return value. That is convenient for cheap, pure work, such as computing the total duration of a pattern from an array of timings, but it is the wrong tool for anything slow: - disk or network I/O blocks the JavaScript thread; - long computation freezes every JavaScript-driven interaction; - UI-affine platform calls may need the main thread, which a synchronous call cannot move to. ## AsyncFunction: Promise and a queue **`AsyncFunction`** always returns a **`Promise`**. The Expo docs recommend it for I/O, for long operations and for work that must run on a specific thread. Its behaviour: 1. By default the native body is **dispatched off the JavaScript thread**. 2. `.runOnQueue(.main)` on iOS or `.runOnQueue(Queues.MAIN)` on Android moves it to the **main thread**, which UI APIs need. `expo-haptics` does this on iOS, and on Android for `performHapticFeedback`, which its source notes is a silent no-op on the default dispatcher. 3. Returning a value **resolves** the Promise; **throwing rejects** it. 4. Declaring a `Promise` as the last closure argument lets native code resolve later, for example from a callback. 5. On Android, `AsyncFunction("name") Coroutine { ... }` accepts a Kotlin **suspend** body bound to the module's coroutine scope. Every function component accepts **up to eight arguments**, because the DSL is generated per arity in both Swift and Kotlin. ## Arguments are typed The closure signature drives conversion: primitives, arrays, dictionaries, `Record` structs with `@Field` members and `Enumerable` enums are converted and validated before your code runs, so a wrong type becomes a JavaScript error instead of a native crash. ## Mistakes reviewers catch - **Slow work in `Function`**: reading a file or waiting on a platform callback inside a synchronous function freezes every JavaScript-driven gesture and animation until it returns. - **UI calls on the default queue**: some platform APIs silently do nothing off the main thread; `expo-haptics` documents exactly this for `performHapticFeedback` on Android. - **A `Constant` for changing state**: the cached value goes stale after the first read. - **Exceptions outside the closure**: throwing from a callback that fires later, rather than inside the `AsyncFunction` body or through its `Promise` argument, cannot reject the JavaScript Promise. ## Choosing for a haptics pattern API - `Constant("maxSteps")`: a fixed limit. - `Property("isAvailable")`: can change if the user disables vibration. - `Function("patternLength")`: cheap arithmetic on an array. - `AsyncFunction("playPatternAsync")`: talks to the platform haptics API, so it returns a Promise and, where the API is main-thread bound, runs on the main queue.

  • Why is a Property, not a Constant, right for 'is vibration available'?
    A `Constant` is computed on first access and cached for the life of the module, so it would keep the first answer even after the user changes a system setting. A `Property` runs its getter on every read, so each access reflects the current state.
  • What happens when an AsyncFunction's native body throws?
    The Promise returned to JavaScript rejects with an error describing the native exception, so the caller handles it with `try`/`catch` around `await` or with `.catch`. This holds for exceptions thrown inside the closure; one thrown later from an unrelated callback cannot reach the Promise.

saying these in an interview costs you the question

  • A Function call runs on a background thread, so it never blocks JavaScript.
  • AsyncFunction bodies always run on the main thread by default.
  • A Constant's closure runs every time JavaScript reads the value.
  • Property is the same as Constant with a different name.
  • Native functions accept any number of arguments.