Fix: ‘Syntax error, unrecognized expression’ in WKWebView

Fix: 'Syntax error, unrecognized expression' in WKWebView

Written by

in

Quick answer: This error occurs when a string containing JSON or raw text retrieved via jQuery’s .val() is passed directly into a jQuery selector like $(text). To fix it, inspect your script for $(variable) selector calls and parse the string directly using JSON.parse() instead of wrapping it in jQuery.

Why this happens

When working with hybrid iOS applications—such as those built with Capacitor, Cordova, or custom WKWebView wrappers—you may encounter the error Error: Syntax error, unrecognized expression: followed by your text or JSON payload. This error originates from jQuery’s internal Sizzle selector engine, not from WKWebView or the iOS keyboard extension.

It commonly surfaces when an iOS keyboard extension inputs text via textDocumentProxy.insertText(), and the web application attempts to validate or process that text. While developers often assume the text reading method (such as $("#input").val()) is broken or that character encoding from iOS introduced hidden control characters, .val() returns plain strings normally.

Instead, the error triggers when the retrieved string is accidentally wrapped in a jQuery selector call, such as $(myString), $(myString).is(...), or find(myString). When jQuery receives a string that starts with characters like {, [, or quotes rather than standard HTML or a valid CSS selector, Sizzle tries to parse it as a DOM query and throws Error: Syntax error, unrecognized expression.

Additionally, using textDocumentProxy.insertText() to transfer payloads (such as user statistics or JSON data) between a custom keyboard extension and its container app is an anti-pattern that creates fragile text parsing workflows.

How to fix it

1. Remove the string variable from jQuery selector calls

Search your JavaScript codebase or web view controller for occurrences where the extracted text is treated as a DOM element. A common pattern is using $() as a shorthand existence check or truthiness assertion:

// INCORRECT: Wrapping the string value in $() treats it as a CSS selector
let rawInput = $("#exportContainer").val();
if ($(rawInput) && rawInput.trim().length > 0) {
  let sanitized = rawInput.trim();
  // process sanitized data
}

Because rawInput contains raw text or JSON, passing it into $(rawInput) causes jQuery to parse the data as a CSS selector. Replace the check with standard JavaScript truthiness:

// CORRECT: Validate string presence without passing it to jQuery
let rawInput = $("#exportContainer").val();
if (typeof rawInput === "string" && rawInput.trim().length > 0) {
  let sanitized = rawInput.trim();
  // process sanitized data
}

2. Parse the string directly with JSON.parse()

If the text typed into the field is structured JSON data, read the string and parse it directly with JSON.parse(). Wrap the operation in a try...catch block to guard against truncated text:

function parseExportedData() {
  const rawText = $("#exportContainer").val();

  if (!rawText) {
    console.warn("Textarea is empty.");
    return null;
  }

  try {
    const payload = JSON.parse(rawText);
    return payload;
  } catch (error) {
    console.error("Failed to parse JSON from input:", error);
    return null;
  }
}

If you prefer vanilla DOM manipulation without jQuery entirely, access the value using standard properties:

const textarea = document.getElementById("exportContainer");
if (textarea) {
  const rawText = textarea.value;
  // parse or process rawText
}

3. Share data via App Groups instead of typing JSON into text fields

Simulating keyboard typing with textDocumentProxy.insertText() to transmit data from a keyboard extension into an app is brittle. If both your host app and keyboard extension belong to the same App Group, you can share data directly through a shared container rather than exposing raw JSON in an input field.

Ensure both targets have the App Groups capability enabled in Xcode with an identifier such as group.com.example.app.

In your keyboard extension (UIInputViewController), write the payload directly to the shared UserDefaults container:

import UIKit

class KeyboardViewController: UIInputViewController {
    private let appGroupID = "group.com.example.app"

    func exportUsageData(jsonString: String) {
        guard let sharedDefaults = UserDefaults(suiteName: appGroupID) else {
            print("Failed to access shared App Group.")
            return
        }

        sharedDefaults.set(jsonString, forKey: "PendingExportData")
        sharedDefaults.synchronize()
    }
}

In your main iOS container application (or within a custom native Capacitor plugin / bridge), read the shared data directly without involving web view inputs:

import Foundation

struct SharedDataManager {
    static let appGroupID = "group.com.example.app"

    static func fetchPendingExport() -> String? {
        guard let sharedDefaults = UserDefaults(suiteName: appGroupID) else {
            return nil
        }

        let data = sharedDefaults.string(forKey: "PendingExportData")
        // Clear the flag after reading
        sharedDefaults.removeObject(forKey: "PendingExportData")
        return data
    }
}

For Capacitor or native web views, expose this via a simple bridge plugin using WKScriptMessageHandler or Capacitor’s native plugin API to send the string straight to JavaScript as an event.

How to prevent it

  • Never wrap arbitrary text in $(): Only pass element references, CSS selectors, or HTML strings intended for creation into jQuery’s factory function.
  • Use TypeScript or strict linting: Configure ESLint rules to enforce strict type checking so string variables are not conflated with DOM elements or jQuery collections.
  • Avoid using input fields as IPC channels: Transfer data between App Extensions and companion apps using App Groups (UserDefaults(suiteName:) or shared container directories) rather than the keyboard proxy.
  • Validate JSON before parsing: Always guard JSON parsing routines with try...catch blocks to prevent unhandled runtime exceptions inside WKWebView.

FAQ

Why does the same code work when copied and pasted manually?

Pasting manually might trigger a different event pipeline or omit leading and trailing whitespace and escape sequences that caused a downstream selector function to be reached.

Can textDocumentProxy inject toxic or hidden characters?

textDocumentProxy.insertText passes UTF-16 Swift strings cleanly, but if the string includes unescaped quotes or newline characters, downstream string concatenation in JavaScript can break syntax if inserted into dynamic script blocks.

Does this error mean WKWebView does not support jQuery?

No. WKWebView supports jQuery and all modern JavaScript libraries. The error is strictly a JavaScript runtime exception thrown by jQuery’s Sizzle engine when non-selector strings are passed into selector parameters.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.