When trying to use expo-av’s Audio.Sound class in a ClojureScript React Native app (using shadow-cljs), we encountered a persistent error:
TypeError: Cannot read property ‘prototype’ of undefined
This error occurred when trying to:
- Call
new Audio.Sound()- FAILS - Call
Audio.Sound.createAsync()- FAILS - Any attempt to instantiate the Sound class normally - FAILS
The Sound constructor has an incompatibility with the ClojureScript/shadow-cljs environment. The constructor function exists, has a valid prototype with all the expected methods (playAsync, pauseAsync, etc.), but fails when invoked with new.
Instead of using the constructor, we bypass it entirely using Object.create:
// Instead of: new Audio.Sound() - which fails
// Use: Object.create(Audio.Sound.prototype) - which works!
const sound = Object.create(Audio.Sound.prototype);
await sound.loadAsync({ uri: audioUrl }, { shouldPlay: true });(defn create-sound-instance []
“Creates a Sound instance using Object.create to bypass constructor bug”
(let [expo-av (js/require “expo-av”)
Audio (.-Audio expo-av)
Sound (.-Sound Audio)]
(.create js/Object (.-prototype Sound))))
(defn load-and-play-audio! [url]
“Loads and plays audio from URL using the workaround”
(let [sound-instance (create-sound-instance)]
(-> (.loadAsync sound-instance
#js {:uri url}
#js {:shouldPlay true})
(.then (fn [result]
(js/console.log “Audio loaded!” result)
sound-instance))
(.catch (fn [error]
(js/console.error “Load error:” error))))));; FAILS with prototype error
(let [Sound (.. expo-av -Audio -Sound)]
(new Sound));; FAILS with prototype error
(.createAsync Sound
#js {:uri url}
#js {:shouldPlay false});; FAILS with prototype error
(js/Reflect.construct Sound #js [])// FAILS with prototype error
const BoundSound = Audio.Sound.bind(Audio);
new BoundSound();// FAILS with prototype error
Audio.Sound.createAsync.call(Audio.Sound, source, status);;; Returns nil in compiled context
(def ExpoAV (js/require “expo-av”))In ClojureScript with shadow-cljs, use string requires in the ns declaration:
(ns example.audio
(:require [“expo-av” :as expo-av])) ; This compiles but expo-av is nil at runtime
;; Instead, use js/require at runtime:
(let [expo-av (js/require “expo-av”)]
...);; Use nested property access carefully
(let [expo-av (js/require “expo-av”)
Audio (.-Audio expo-av) ; Get Audio object
Sound (.-Sound Audio)] ; Get Sound constructor
...)(-> (.loadAsync sound-instance source-map status-map)
(.then (fn [result]
; Handle success
sound-instance)) ; Return the instance for chaining
(.catch (fn [error]
; Handle error
(js/console.error “Error:” error))))Use atoms for mutable sound state:
(defonce current-sound (atom nil))
(defn load-sound! [url]
(let [sound (create-sound-instance)]
(-> (.loadAsync sound #js {:uri url} #js {:shouldPlay false})
(.then #(reset! current-sound sound)))))Once created with Object.create, these methods all work:
loadAsync(source, initialStatus)- Load audio from URIplayAsync()- Start playbackpauseAsync()- Pause playbackstopAsync()- Stop and reset to beginningsetPositionAsync(millis)- Seek to positionsetRateAsync(rate, shouldCorrectPitch)- Change playback speedsetVolumeAsync(volume)- Set volume (0.0 to 1.0)setIsLoopingAsync(isLooping)- Enable/disable loopinggetStatusAsync()- Get current playback statusunloadAsync()- Release resources
✅ Successfully loaded and played IT Revolution podcast on iOS device ✅ Audio quality is perfect ✅ All playback controls work as expected ✅ No memory leaks or performance issues observed
- This workaround is specific to ClojureScript/shadow-cljs environments
- The issue does NOT occur in regular JavaScript/TypeScript React Native apps
- Always use Object.create pattern when working with expo-av Sound in ClojureScript
- Remember to call unloadAsync() when done to free resources
- The expo-av deprecation warning can be ignored - it still works
August 19, 2025
- expo-av: 15.1.7
- React Native: 0.71.19
- ClojureScript: 1.11.60
- shadow-cljs: (react-native target)
- iOS: 18.6