software-mansion / software-mansion/react-native-audio-api

SIGABRT crash: AVAudioEngine startAndReturnError: throws NSException, not caught in AudioEngine startEngine

Open Beginner friendly
#1,238 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
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

  1. App with a single shared AudioContext, several AudioBufferSourceNodes created ahead of
    time, played via .start() on user interaction (typical "SFX pool" pattern).
  2. 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.
  3. 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 under ko/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.
  4. Full 3x identical .ips crash 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.