pmndrs / react-three-fiber

🇨🇭 A React renderer for Three.js
https://docs.pmnd.rs/react-three-fiber
MIT License
26.96k stars 1.53k forks source link

Handle exceptions in native Canvas renderFrame in React Error Boundary #3303

Open navignaw opened 1 month ago

navignaw commented 1 month ago

I'm wrapping a @reat-three/fiber/native Canvas element in an ErrorBoundary, but it's not properly handling errors.

Here's a raw (minified) stack trace for an error in production:

TypeError: Cannot read property 'name' of undefined
  at WebGLUniforms(/node_modules/three/build/three.cjs:19298:48)
  at onFirstUse(/node_modules/three/build/three.cjs:20388:37)
  at anonymous(/node_modules/three/build/three.cjs:20402:14)
  at setProgram(/node_modules/three/build/three.cjs:30556:42)
  at anonymous(/node_modules/three/build/three.cjs:29438:30)
  at renderObject(/node_modules/three/build/three.cjs:30240:29)
  at renderObjects(/node_modules/three/build/three.cjs:30209:18)
  at renderScene(/node_modules/three/build/three.cjs:30070:49)
  at anonymous(/node_modules/three/build/three.cjs:29888:16)
  at anonymous(/node_modules/@react-three/fiber/native/dist/react-three-fiber-native.cjs.prod.js:248:22)
  at render$1(/node_modules/@react-three/fiber/dist/index-f0ac6f0d.cjs.prod.js:1568:67)
  at loop(/node_modules/@react-three/fiber/dist/index-f0ac6f0d.cjs.prod.js:1592:27)
  at _callTimer(/node_modules/react-native/Libraries/Core/Timers/JSTimers.js:113:15)
  at callTimers(/node_modules/react-native/Libraries/Core/Timers/JSTimers.js:359:17)
  at apply(native)
  at __callFunction(/node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:433:34)
  at anonymous(/node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:113:26)
  at __guard(/node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:368:11)
  at callFunctionReturnFlushedQueue(/node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:112:17)

the specific react-three-fiber-native.cjs.prod.js line maps to this renderFrame function:

          // Bind render to RN bridge
          const context = state.gl.getContext() as ExpoWebGLRenderingContext
          const renderFrame = state.gl.render.bind(state.gl)
          state.gl.render = (scene: THREE.Scene, camera: THREE.Camera) => {
>           renderFrame(scene, camera)
            context.endFrameEXP()
          }

Unfortunately because this is in a callback that's called by the three.js code, any exceptions are uncaught and crash the native app.

One suggestion would be to wrap it in a try/except and call the setError function:

          // Bind render to RN bridge
          const context = state.gl.getContext() as ExpoWebGLRenderingContext
          const renderFrame = state.gl.render.bind(state.gl)
          state.gl.render = (scene: THREE.Scene, camera: THREE.Camera) => {
+           try {
+             renderFrame(scene, camera)
+             context.endFrameEXP()
+           } catch (error) {
+             setError(error)
+           }
          }

and this will get automatically thrown and therefore handled by a wrapping ErrorBoundary.

let me know if this sounds reasonable!

Possibly related: #3181

CodyJasonBennett commented 1 month ago

I think we have a general problem of errors not being propagated to the error boundary if thrown from a loop or the reconciler itself. We could use a correction here. #3181 is far simpler to fix, just more complex to test.

navignaw commented 1 month ago

Do you have a suggestion for a catch-all solution that would work for both #3181 and this issue? I'd be happy to help test with my own project.