CORE JSC

International Technology Partnership

React Native

Fixing React Native Reanimated Crashes: "Tried to Synchronously Call a Non-Worklet Function"

An animation that worked perfectly in development suddenly crashes with "Tried to synchronously call a non-worklet function." The animated style logic looks fine; the actual problem is a plain JavaScript function being called from a context that only runs on the UI thread, where regular JS functions don't exist.

Core JSC Team·August 6, 2026
React NativeReanimatedWorkletsAnimationCrash

The Problem

A React Native app using Reanimated crashes — sometimes only on a physical device or in a release build, not always in the Metro dev server — with an error like Tried to synchronously call a non-worklet function on the UI thread. The animation code that triggers it, usually inside useAnimatedStyle, useDerivedValue, or a gesture handler callback, looks unremarkable: no obvious syntax error, nothing that looks different from other animations already working fine elsewhere in the app.

Why It Happens

Reanimated runs certain code on a completely separate JS runtime, on the UI thread

Reanimated's whole performance model depends on running animation logic — worklets — directly on the UI thread in their own lightweight JavaScript runtime, so animations keep running smoothly even while the main JS thread is busy. A worklet is a specially marked, specially compiled function; a regular JavaScript function, defined normally, simply doesn't exist on that runtime and can't be called from it directly.

A function gets called inside a worklet without being compiled as one

The Babel plugin react-native-reanimated/plugin automatically detects and compiles functions used inside Reanimated hooks into worklets — but it can only do this for functions whose definitions it can actually see and transform at build time. A function imported from a third-party library, a class method, or a function assembled dynamically won't get the worklet transformation, so calling it directly from inside useAnimatedStyle tries to execute plain JS on a runtime that doesn't support it.

A callback meant for the JS thread gets called directly instead of dispatched to it

Calling a React state setter, a prop callback passed down from a parent component, or any function meant to run in the app's normal JS environment directly from inside a worklet is a very common instance of the same underlying mistake — that function was never compiled as a worklet (it isn't meant to be one), so it needs to be explicitly dispatched back to the JS thread rather than called as if it were already running there.

The Fix

1. Wrap any call to a JS-thread function from inside a worklet in runOnJS

import { runOnJS } from "react-native-reanimated";

const handleGestureEnd = (finished: boolean) => {
  setIsAnimating(false); // a normal React state setter — belongs on the JS thread
};

const gesture = Gesture.Pan().onEnd(() => {
  "worklet";
  runOnJS(handleGestureEnd)(true);
});

runOnJS explicitly hands the call back to the JS thread instead of trying to execute the plain function on the UI thread's worklet runtime — this is the correct pattern for state setters, prop callbacks, analytics calls, or any other function that isn't itself a worklet.

2. Mark a custom helper function as a worklet explicitly if it's meant to run on the UI thread

function clampValue(value: number, min: number, max: number) {
  "worklet";
  return Math.min(Math.max(value, min), max);
}

The "worklet" directive at the top of a function tells the Babel plugin to compile it for the UI thread. This is required for any helper function called directly from inside useAnimatedStyle, useDerivedValue, or a gesture callback — including small utility functions defined in the same file, not just ones imported from elsewhere.

3. Confirm the Reanimated Babel plugin is last in the plugins array and the Metro cache is clean

// babel.config.js
module.exports = {
  presets: ["module:@react-native/babel-preset"],
  plugins: [
    // ...other plugins
    "react-native-reanimated/plugin", // must be listed last
  ],
};
npx react-native start --reset-cache

The Reanimated Babel plugin must run after other plugins that might transform the same code, and a stale Metro cache can keep serving a bundle compiled before the plugin was correctly configured — both silently produce functions that were never actually compiled into worklets, even though the source code looks correct.

4. Don't call unmarked third-party functions directly inside a worklet

A function imported from a library that wasn't written with Reanimated worklets in mind (most general-purpose utility libraries) can't be compiled into a worklet by the Reanimated Babel plugin, since the plugin only transforms code within the project. Either reimplement the small piece of logic actually needed as a local, explicitly marked worklet, or move the call to the JS thread via runOnJS if the library call itself doesn't need to run on the UI thread.

Why This Works

Every fix here addresses the same underlying rule: code that runs on the UI thread's worklet runtime must actually be compiled as a worklet, and code that belongs on the normal JS thread must be explicitly dispatched there via runOnJS, never called as if the two runtimes were interchangeable. Marking helper functions with the "worklet" directive, wrapping JS-thread callbacks in runOnJS, and keeping the Babel plugin correctly configured all ensure that every function actually being called from a worklet context is one the UI thread runtime can genuinely execute.

Conclusion

A "non-worklet function" crash in Reanimated means some function is being called directly on the UI thread runtime without ever having been compiled into a worklet — never a random or unfixable failure. Wrap calls to JS-thread functions (state setters, prop callbacks) in runOnJS, mark helper functions meant to run on the UI thread with the "worklet" directive, and confirm the Reanimated Babel plugin is correctly configured and the Metro cache is fresh.