software-mansion / software-mansion/react-native-audio-api
SIGABRT crash: AVAudioEngine startAndReturnError: throws NSException, not caught in AudioEngine startEngine
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 839
- Forks
- 92
- Avg merge
- 4d 15h
- Merged PRs (30d)
- 27
Description
Summary
-[AudioEngine startEngine] calls [self.audioEngine startAndReturnError:&error] and only
checks the NSError out-parameter. If AVAudioEngine.startAndReturnError: fails via an
Objective-C exception instead of populating error (which AVAudioEngine does in some
internal states — e.g. graph not fully attached/prepared, or a race between stop/attach/start),
the exception is never caught. It propagates past the JS boundary (uncatchable from JS try/catch,
since it isn't a JS exception) and the process calls abort() — SIGABRT, full app crash, not a
recoverable JS error.
Because the very first engine start tends to happen on a cold path (stop → attach graph → start),
this is most likely to fire on an app's first sound effect play — i.e. exactly when a user is
tapping fastest (game start, first move, etc).
Environment
react-native-audio-api: 0.13.2 (confirmed unrelated to the two fixes in 0.13.3 — prebuilt
binary download + render queue event resolution)- React Native: 0.83.6
- React: 19.2.0
- Expo: ~55.0.28
- iOS Simulator: iPhone 16e, iOS 26.2
- macOS: 26.5.2 (Xcode 26)
- New Architecture: enabled
Crash signature
Exception Type: EXC_CRASH (SIGABRT)
Thread: com.facebook.react.runtime.JavaScript
Symbolicated with the app's dSYM:
audioapi::AudioBufferSourceNode::start(double, double, double) AudioBufferSourceNode.cpp:93
audioapi::AudioScheduledSourceNode::start(double) AudioScheduledSourceNode.cpp:31
audioapi::AudioContext::start() AudioContext.cpp:119
audioapi::AudioContext::tryStartDriver() AudioContext.cpp:56
audioapi::IOSAudioPlayer::start() IOSAudioPlayer.mm:129
-[NativeAudioPlayer start] NativeAudioPlayer.m:84
-[NativeAudioPlayer startPlaybackGraph:] NativeAudioPlayer.m:32
-[AudioEngine startIfNecessary] AudioEngine.mm:504
-[AudioEngine startEngine] AudioEngine.mm:470 ← crash site
↓ abort() inside AVFAudio / AudioToolboxCore / libEmbeddedSystemAUs
Reproduced identically 3/3 times (same offsets down to the byte) with rapid successive
AudioBufferSourceNode.start() calls — i.e. a user tapping quickly enough that a second SFX
play triggers tryStartDriver() → AudioEngine startEngine while the engine is mid
stop/attach/start from the first play.
The relevant source (ios/audioapi/ios/system/AudioEngine.mm, current main):
- (bool)startEngine
{
NSError *error = nil;
...
[self.audioEngine prepare];
[self.audioEngine startAndReturnError:&error]; // <-- can throw NSException, not just set error
if (error != nil) {
NSLog(@"Error while starting the audio engine: %@", [error debugDescription]);
return false;
}
...
}
There's no @try/@catch around prepare/startAndReturnError:, so any NSException thrown
by AVAudioEngine (as opposed to an NSError) is unhandled and reaches std::terminate/abort.
Repro steps
- App with a single shared
AudioContext, severalAudioBufferSourceNodes created ahead of
time, played via.start()on user interaction (typical "SFX pool" pattern). - From a cold app launch, trigger two or more
.start()calls in quick succession (within
the same event loop tick / a few hundred ms) — e.g. a user tapping a game board rapidly right
after opening the app, before the engine has had a chance to start once and settle. - Observed most reliably on iOS Simulator with the system locale set to
zh-Hans(Simplified
Chinese) — reproduced in 3/3 attempts there, vs. 0/2 with the same tap pattern underko/ja
locales. We don't have a theory for why locale would matter and don't want to over-claim
causation from a small sample — flagging it in case it's a useful signal (maybe just timing
noise, maybe a locale-dependent AVAudioSession/AVSpeechSynthesizer‑adjacent init path — pure
speculation). Turning SFX off entirely avoided the crash in every attempt, which does
confirm the SFX playback path is the trigger, independent of locale. - Full 3x identical
.ipscrash reports (frame offsets match exactly) available on request —
happy to attach if useful.
Workaround we shipped
We moved our first-ever AudioBufferSourceNode.start() call (a silent, zero-volume buffer) to
run once during app init / title screen, well before the player can interact with the game
board. This forces the risky cold engine start to happen at a moment when nothing else can call
.start() concurrently. After this change we could no longer reproduce the crash with the same
(and a more aggressive) tap pattern that reproduced it reliably before.
This is a mitigation, not a fix — if the underlying AVAudioEngine.startAndReturnError: call
ever throws for a legitimate reason after warm-up (e.g. an audio route change, interruption
recovery), the app would still abort instead of getting a catchable error.
Suggested fix
Wrap the prepare/startAndReturnError: call in @try/@catch in -[AudioEngine startEngine]
and surface the caught exception the same way an NSError currently is (log + return false),
so callers get a recoverable failure instead of a process abort:
@try {
[self.audioEngine prepare];
[self.audioEngine startAndReturnError:&error];
} @catch (NSException *exception) {
NSLog(@"Exception while starting the audio engine: %@", exception);
return false;
}
Happy to send a PR with this if that's useful — wanted to file the report with the
symbolicated stack and repro conditions first in case there's more context on the intended
error-handling contract here.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in ios/audioapi/ios/system/AudioEngine.mm at -[AudioEngine startEngine], especially the prepare and startAndReturnError: calls around line 470. Review the existing NSError handling and ensure an AVAudioEngine exception produces the same recoverable false result instead of aborting. Validate with the rapid successive AudioBufferSourceNode.start() reproduction described in the issue.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- ios, objective-c, react-native
- Domain
- audio-video-rtc, mobile-dev
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100