Xcode 26.4 WKWebView evaluateJavaScript

In an Xcode 26.4 + iOS 26.x environment, I called evaluateJavaScript inside webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) and got the following error:

Printing description of error:
▿ Optional<Error>
  - some : Error Domain=WKErrorDomain Code=4 "A JavaScript exception occurred" UserInfo={WKJavaScriptExceptionLineNumber=0, WKJavaScriptExceptionMessage=TypeError: undefined is not a function, WKJavaScriptExceptionColumnNumber=0, NSLocalizedDescription=A JavaScript exception occurred})

However, this worked fine in earlier versions before Xcode 26.4 (> 26.0 and < 26.4, though I do not remember the exact version), still with iOS 26.x.

It also works fine in an Xcode 26.4 + iOS 18.x environment.

And if I add a slight delay using DispatchQueue.main.asyncAfter in the Xcode 26.4 + iOS 26.x environment, then it works without any issue.

So what exactly is going on here? I would really appreciate an explanation.

Answered by DTS Engineer in 888694022

Thanks for the precise reproducer — isolating the failure to the Xcode 26.4 SDK + iOS 26.x runtime combination, and confirming that a delay makes it work, narrows the cause.

The pattern you're describing — evaluateJavaScript from webView(_:didCommit:) failing with "TypeError: undefined is not a function," and working when delayed — points to a delegate-timing issue. didCommit is the wrong lifecycle stage for evaluating JavaScript that depends on the page's own scripts being defined.

The ordering from WKNavigationDelegate (https://developer.apple.com/documentation/webkit/wknavigationdelegate):

  1. didStartProvisionalNavigation — request started
  2. didReceiveServerRedirect… — any redirects
  3. decidePolicyFor navigationResponse — response headers received
  4. didCommit — response committed; document creation starting
  5. didFinish — page fully loaded; page scripts have executed

At didCommit time, the response has been committed and the document is being constructed, but the <script> elements in the page haven't necessarily run yet. If your evaluateJavaScript call is invoking a function defined by one of the page's own scripts, that function may not exist in the JS context at the moment you call it — which is what undefined is not a function is reporting.

webView(_:didFinish:) is the documented "page has finished loading" callback, including initial script execution. That's the natural place to call evaluateJavaScript against page-defined symbols. The DispatchQueue.main.asyncAfter workaround approximates the same thing on a fixed timer rather than an event signal. It works because by the time the delay elapses, the page's scripts have usually finished running. The timer length is a guess, though; an event-driven signal is more reliable.

Moving the evaluateJavaScript call to didFinish should resolve it deterministically rather than depending on the delay being long enough on a given device or network.

If the JS you're evaluating only depends on standard browser globals (no page-defined functions), please share the string — there'd be a different cause to investigate.

Thanks for the precise reproducer — isolating the failure to the Xcode 26.4 SDK + iOS 26.x runtime combination, and confirming that a delay makes it work, narrows the cause.

The pattern you're describing — evaluateJavaScript from webView(_:didCommit:) failing with "TypeError: undefined is not a function," and working when delayed — points to a delegate-timing issue. didCommit is the wrong lifecycle stage for evaluating JavaScript that depends on the page's own scripts being defined.

The ordering from WKNavigationDelegate (https://developer.apple.com/documentation/webkit/wknavigationdelegate):

  1. didStartProvisionalNavigation — request started
  2. didReceiveServerRedirect… — any redirects
  3. decidePolicyFor navigationResponse — response headers received
  4. didCommit — response committed; document creation starting
  5. didFinish — page fully loaded; page scripts have executed

At didCommit time, the response has been committed and the document is being constructed, but the <script> elements in the page haven't necessarily run yet. If your evaluateJavaScript call is invoking a function defined by one of the page's own scripts, that function may not exist in the JS context at the moment you call it — which is what undefined is not a function is reporting.

webView(_:didFinish:) is the documented "page has finished loading" callback, including initial script execution. That's the natural place to call evaluateJavaScript against page-defined symbols. The DispatchQueue.main.asyncAfter workaround approximates the same thing on a fixed timer rather than an event signal. It works because by the time the delay elapses, the page's scripts have usually finished running. The timer length is a guess, though; an event-driven signal is more reliable.

Moving the evaluateJavaScript call to didFinish should resolve it deterministically rather than depending on the delay being long enough on a given device or network.

If the JS you're evaluating only depends on standard browser globals (no page-defined functions), please share the string — there'd be a different cause to investigate.

Thanks for the clarification in your comment above. That changes the diagnostic picture. And to restate the didFinish contract more precisely than I did in my earlier reply: the documentation for webView(_:didFinish:) describes it as "Tells the delegate that navigation is complete": a statement about the navigation state machine, not about what JavaScript has executed in the page. Whether any particular function defined by the page's scripts is bound in the JavaScript context at that moment depends on how those scripts are structured.

If evaluateJavaScript from didFinish is reporting undefined is not a function, the symbol being called isn't bound in the JS context at that moment. Some common reasons:

  • The function is defined inside a DOMContentLoaded or window.onload handler, or inside a Promise / setTimeout / requestAnimationFrame callback that hasn't run at the moment didFinish fires.
  • The function is in a <script defer> or <script async> tag, or in an external .js file that hasn't finished loading and executing.
  • The function is added by a script that itself loads more script (for example, document.createElement('script') with an onload handler that defines the symbol).
  • The symbol is a property on an object that's still being constructed asynchronously when the call is made.

The DispatchQueue.main.asyncAfter workaround succeeding is consistent with all of those: by the time the delay elapses, whatever later step binds the symbol has had time to run.

To narrow it down, a few things would help:

  • The exact string passed to evaluateJavaScript, including the function name being called.
  • Where that function is defined in the page: inline <script>, external .js via src=, defer or async, inside an event handler such as DOMContentLoaded or window.onload, or added dynamically.
  • How the page is loaded into the web view (load(URLRequest:), loadFileURL(_:allowingReadAccessTo:), or loadHTMLString(_:baseURL:)).
  • Whether any WKUserScript is being injected, or whether a non-default WKContentWorld is used for the evaluation.
  • The exact iOS 26.x builds where it fails and where it works (for example 26.0, 26.1, 26.4).

The canonical diagnostic step here is attaching Safari Web Inspector to the WKWebView inside your running app (Safari > Develop > [device] > [your app's web view]). With Xcode paused at a breakpoint just before the evaluateJavaScript call, the Web Inspector JS console lets you check typeof yourFunction at that exact moment. Repeating the check after a few seconds, and watching Web Inspector's Network and Timeline panels for what runs in between, will identify what's binding the symbol after didFinish.

Xcode 26.4 WKWebView evaluateJavaScript
 
 
Q