RORK LABJP
MAX — Rork Max generates native Swift apps across iPhone, iPad, Watch, TV, Vision Pro, and iMessageNATIVE — It reaches AR/LiDAR scanning, Metal 3D games, widgets, Live Activities, and on-device Core MLFUNDING — Rork raised $2.8M from a16z, with 743K monthly visits and 85% growthPRICING — It's free to start, with paid plans beginning at $25 per monthFLOW — Describe your idea in plain English to get working code, a shareable test link, and iOS/Android buildsCOMPARE — The original Rork builds cross-platform apps on Expo/React Native; choose the right tool per goalMAX — Rork Max generates native Swift apps across iPhone, iPad, Watch, TV, Vision Pro, and iMessageNATIVE — It reaches AR/LiDAR scanning, Metal 3D games, widgets, Live Activities, and on-device Core MLFUNDING — Rork raised $2.8M from a16z, with 743K monthly visits and 85% growthPRICING — It's free to start, with paid plans beginning at $25 per monthFLOW — Describe your idea in plain English to get working code, a shareable test link, and iOS/Android buildsCOMPARE — The original Rork builds cross-platform apps on Expo/React Native; choose the right tool per goal
Articles/Dev Tools
Dev Tools/2026-04-24Intermediate

Reanimated Worklet Errors in Rork Apps — Six Things to Check Before You Panic

React Native Reanimated throws a lot of worklet errors the moment you add it to a Rork project. This walkthrough covers the six most common causes, from a missing Babel plugin to closure capture bugs, in the order you should investigate them.

Rork465Reanimated9worklettroubleshooting66React Native189animation2

You add React Native Reanimated to your Rork project expecting a few slick transitions, and instead you get a red screen full of Reanimated 2 failed to create a worklet or Tried to synchronously call function from a different thread. If that scenario sounds familiar, you are not alone — I once spent half a day tracking down a worklet error that only appeared on a physical device after an EAS build, even though everything ran fine inside Rork Companion.

Reanimated worklets look like ordinary functions, but they are compiled by a Babel plugin to run on the UI thread. That transformation is what makes them fast, and it is also the reason the same-looking code can work in one place and fail in another. Below is the order I now use to diagnose worklet-related errors in Rork projects. It targets Reanimated v3, but the same logic applies to v2.

Identify Which Layer Is Actually Failing

Worklet errors fall into three layers, and knowing which one you are in saves hours of guesswork.

  • Build-time failure — the Babel plugin never ran, so your dev build fails at boot.
  • Initial render failure — calls like useAnimatedStyle blow up the first time the component mounts.
  • Runtime thread-boundary failure — gestures or animations run, but the moment they fire you see a crash.

Read the error message before touching any code. Reanimated 2 failed to create a worklet, maybe you forgot to add Reanimated's babel plugin? is a layer-one symptom. A ReferenceError inside a worklet points at layer two. Anything mentioning "synchronously call function" or "different thread" is layer three. Skipping this step is how people fix the same issue three different ways and still end up stuck.

1. Make Sure react-native-reanimated/plugin Is in babel.config.js

This is the single most common issue. Rork-generated projects ship with only the Expo preset, and installing Reanimated manually does not touch the Babel config.

// Bad — no Reanimated plugin configured
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ["babel-preset-expo"],
  };
};
// Good — plugin added as the LAST entry
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ["babel-preset-expo"],
    plugins: ["react-native-reanimated/plugin"],
  };
};

The plugin must be last in the array — the official docs emphasise this because the worklet transform has to run after everything else. After updating the file, restart Metro with npx expo start --clear so the transform cache is rebuilt. If you skip the cache clear, the plugin will look installed but still not apply.

A subtle follow-on issue: even after the plugin is in place and the cache is cleared, a stale Metro process can keep serving the old bundle. If you still see the plugin error after a restart, kill all running Metro instances with killall -9 node (or Activity Monitor / Task Manager) before starting Metro again. It sounds excessive, but I have watched the same phantom error survive three restarts until the stray process was killed.

2. Rebuild the Native Layer

Reanimated ships a native module, so updating the JS side alone will not do anything once your bundle includes a previous version. If your animation works in Rork Companion but fails on a real device build, this is almost always the problem.

# Rebuild the dev client from scratch
npx expo prebuild --clean
npx expo run:ios    # or run:android
 
# Clear EAS build cache if the error persists
eas build --platform ios --clear-cache

prebuild --clean regenerates the ios/ and android/ folders. If you have hand-edited anything inside them, stash those changes first. For stock Rork-generated projects you can run it as-is without worry.

3. Don't Call Plain JS Functions From Inside a Worklet

The most important Reanimated rule: only worklets can call worklets. Rork-generated code sometimes drops helper utilities straight into useAnimatedStyle, which compiles fine but crashes the moment the worklet runs.

// Bad — worklet calls a regular JS function
import { useAnimatedStyle } from "react-native-reanimated";
 
function clamp(value: number, min: number, max: number) {
  return Math.min(Math.max(value, min), max);
}
 
const style = useAnimatedStyle(() => {
  // Runtime: "Tried to synchronously call function clamp"
  const opacity = clamp(progress.value, 0, 1);
  return { opacity };
});
// Good — mark the helper as a worklet
function clamp(value: number, min: number, max: number) {
  "worklet";
  return Math.min(Math.max(value, min), max);
}
 
const style = useAnimatedStyle(() => {
  const opacity = clamp(progress.value, 0, 1);
  return { opacity };
});

The "worklet"; directive on the first line tells the Babel plugin to compile the function as a worklet too. I recommend keeping shared helpers in a file like utils/worklets.ts with the directive on every function, so there is no ambiguity when you reuse them.

4. Use runOnJS When You Need the JS Thread

It's tempting to call setState or trigger navigation straight from a worklet, but doing so throws the same cross-thread error as Pattern 3. Wrap the call in runOnJS to hop back to the JS thread explicitly.

// Bad — setState called directly from a worklet
import { useAnimatedGestureHandler } from "react-native-reanimated";
 
const gestureHandler = useAnimatedGestureHandler({
  onEnd: () => {
    setSwipeCount(prev => prev + 1); // JS function called from UI thread
  },
});
// Good — runOnJS bridges back to the JS thread
import { useAnimatedGestureHandler, runOnJS } from "react-native-reanimated";
 
const gestureHandler = useAnimatedGestureHandler({
  onEnd: () => {
    runOnJS(setSwipeCount)(swipeCount + 1);
  },
});

runOnJS serialises values only, so functional updates like setSwipeCount(prev => prev + 1) will not work when passed directly. If you need that pattern, create a small JS-side wrapper that takes the current value and calls the setter internally.

5. Watch Out for Stale Closures

Worklets capture values at the moment they are defined. Reference a React state or prop directly and you will keep seeing the old value — the animation runs, but it never reacts to the latest data. This one is particularly confusing because nothing crashes; the UI just feels broken.

// Bad — worklet captures the initial threshold value
const [threshold, setThreshold] = useState(100);
 
const style = useAnimatedStyle(() => {
  return { opacity: progress.value > threshold ? 1 : 0 };
});

There are two clean ways to fix this. The first is to hold the value in a shared value:

// Good — move threshold into useSharedValue
const threshold = useSharedValue(100);
 
// Update like this
threshold.value = 150;
 
const style = useAnimatedStyle(() => {
  return { opacity: progress.value > threshold.value ? 1 : 0 };
});

The second is useDerivedValue, which is handy when the JS-side value comes from a library you cannot refactor:

// Good — bridge a JS value into a shared value
const thresholdShared = useDerivedValue(() => {
  return threshold;
}, [threshold]);

Don't forget the dependency array — miss it and the bridge silently stops updating. Think of it exactly like useEffect dependencies.

6. Check Expo Go and Hermes Versions

This one is easy to miss. If you test with an older Expo Go client, its bundled Reanimated version may not match your project, and worklets simply fail to run. When Rork Companion and the physical device disagree about whether your animation works, check that your app.json runtimeVersion matches the Expo Go build you have installed.

# Verify Reanimated is on the Expo-recommended version
npx expo install --check
# Expect: react-native-reanimated listed at the SDK-recommended version

Drift away from Expo's recommended version and you may hit unimplemented APIs or mismatched worklet transforms. Running npx expo install react-native-reanimated re-aligns you. For anything beyond quick prototyping, build a proper development client so you stop depending on Expo Go's pinned version altogether — most of the inconsistency problems evaporate once you leave Expo Go behind.

A quick reality check: most Rork users will not hit version mismatches if they stay inside the Rork Companion + EAS workflow without side-installing Reanimated manually. The cases I see most often involve someone upgrading Expo SDK on their own and forgetting to run npx expo install --check afterwards. Treat that command as part of every upgrade and you will dodge this category of bug entirely.

One practical workflow tip: keep a minimal reproduction scene in your project while you are learning Reanimated. A single screen with a spring animation and a pan gesture gives you a sandbox to test each worklet pattern in isolation. When an error appears in your real app, copy the failing snippet into the sandbox and work from there. Diagnosing worklets inside a busy screen full of other logic is how small mistakes turn into full-day debugging sessions.

What to Do Next

Worklet errors look like code problems but are usually build, thread, or cache problems wearing a disguise. The single highest-return move you can make today is to open babel.config.js, confirm react-native-reanimated/plugin is the final entry, and rebuild the dev client with --clear. That alone resolves more than half of the cases I have seen.

If you are moving into more elaborate gesture and animation work, skim Advanced animations in Rork with Reanimated and Gesture Handler and A complete guide to debugging Rork app crashes and white screens. Reading those two together gives you a mental model of where the UI thread ends and the JS thread begins — which is the real key to avoiding worklet errors in the first place.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-06-21
Add Long-Press Drag Reordering to Your Rork Favorites List Without the Jank
A practical walkthrough for retrofitting long-press drag reordering onto a Rork-generated favorites list: keeping re-renders down, respecting the worklet boundary, persisting the order, and avoiding ghost cards and scroll conflicts.
Dev Tools2026-06-12
Your Rork App's 'Documents & Data' Keeps Growing — Taming expo-image's Disk Cache
My wallpaper app's binary was 40 MB, yet 'Documents & Data' had ballooned to 2.4 GB. Here is how I diagnosed expo-image's unbounded disk cache and fixed it with cachePolicy tuning, thumbnail URLs, and generational cache clearing.
Dev Tools2026-04-29
Adding Bottom Sheets to a Rork App — A Practical Guide to @gorhom/bottom-sheet on iOS and Android
Rork's default Modal works for confirmations, but the moment you need multiple snap points or inertial scroll inside the sheet it falls short. This guide walks through dropping @gorhom/bottom-sheet into a Rork project, handling the keyboard, and smoothing out iOS/Android differences.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →