Skip to content

Defer "resume" event in AppPlugin until WebView is ready (prevents JS eval errors after crash) #2357

@smart

Description

@smart

After going down the rabbit hole with AI

Bug Report

Plugin(s)

@capacitor/app

Capacitor Version

❯ npx cap doctor
💊 Capacitor Doctor 💊

Capacitor CLI: 7.x.x
@capacitor/core: 7.x.x
@capacitor/ios: 7.x.x
@capacitor/android: not used
Installed Dependencies:
@capacitor/app: 7.x.x

Platform(s)
• iOS (likely only relevant to iOS since it’s WKWebView related)

Current Behavior

When the app resumes from the background and the WKWebView process was previously killed by the OS (e.g. due to memory pressure), the AppPlugin immediately tries to notifyListeners("resume", ...) — triggering JavaScript evaluation before the WebView has finished reloading.

This results in:

⚡️ JS Eval error A JavaScript exception occurred
🕵️ JS EVAL ATTEMPT: window.Capacitor.triggerEvent('resume', 'document')
⚠️ JS called while WKWebView is still loading

Because the WebView is not yet ready, this call is ineffective and noisy. Worse, it may trigger app logic relying on a fully functional DOM, even though the page hasn’t yet rendered.

Expected Behavior

The "resume" event should not be sent until the WKWebView is fully loaded and ready to execute JS safely. This would mirror how other platform events defer firing until the bridge is available.

Code Reproduction

Not easily reproducible with a clean project, but you can simulate the crash scenario like this:
1. Load your app in Simulator.
2. Push it to background.
3. Manually kill the WebView process (or let the OS do it under pressure).
4. Resume the app.
5. Observe the JavaScript error and stack trace in the Xcode logs.

Alternatively, set up a logging swizzle like this to confirm JS is being evaluated too early:

extension WKWebView {
@objc func swizzled_evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil) {
print("🕵️ JS EVAL ATTEMPT: (javaScriptString.prefix(100))")
if self.isLoading {
print("⚠️ JS called while WKWebView is still loading")
} else {
print("✅ WebView is ready")
}
swizzled_evaluateJavaScript(javaScriptString, completionHandler: completionHandler)
}
}

Other Technical Details
• WKWebView restarts cleanly
• applicationWillEnterForeground + applicationDidBecomeActive used to re-add splash UI while WebView is reloading
• window.Capacitor.fromNative(...) starts firing while page hasn’t re-rendered

Additional Context

A possible solution would be to defer the "resume" event in AppPlugin.load() by checking the bridge’s state and using something like:

self.bridge?.onReady {
self?.notifyListeners("resume", data: nil)
}

Proposed solution
Here’s a ready-to-paste proposed patch you can include in your GitHub issue (or use to open a PR later):

🔧 Proposed Patch (AppPlugin.swift)

// Inside AppPlugin.load()

observers.append(NotificationCenter.default.addObserver(
forName: UIApplication.willEnterForegroundNotification,
object: nil,
queue: OperationQueue.main
) { [weak self] (_) in
guard let self = self else { return }

// Defer "resume" until bridge is ready
if let bridge = self.bridge, bridge.isReady {
    self.notifyListeners("resume", data: nil)
} else {
    self.bridge?.onReady {
        self.notifyListeners("resume", data: nil)
    }
}

})

✅ Why This Fix Helps
• Prevents firing window.Capacitor.triggerEvent('resume', 'document') while the WebView is still reloading.
• Eliminates console noise and misleading errors.
• Aligns with Capacitor’s general behavior of deferring plugin JS events until bridge is ready.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions