A JavaScript string is any sequence of 16-bit values. The spec places no restriction on them, so a string can hold a lone surrogate: a leading surrogate (0xD800–0xDBFF) not followed by a trailing surrogate, or a trailing surrogate (0xDC00–0xDFFF) not preceded by a leading one. Such a string is ill-formed UTF-16 and does not represent valid Unicode text.
They usually come from code that treats strings as units: slice or substring cutting a pair in half, split("") followed by manipulation, or a truncating charAt loop.
Two methods handle them (widely available in browsers since October 2023, per MDN):
str.isWellFormed()returnsfalseif the string contains any lone surrogate.str.toWellFormed()returns a new string with every lone surrogate replaced by U+FFFD (�). It returns a copy even when nothing needed fixing.
Why it matters: APIs that need valid Unicode reject ill-formed input. encodeURI and encodeURIComponent throw a URIError. Encoding to UTF-8 has no representation for a lone surrogate, so TextEncoder substitutes U+FFFD. JSON.stringify escapes lone surrogates ("\ud800") so its output stays valid.
Use isWellFormed() to validate or reject input, and toWellFormed() to sanitize it before passing it to such APIs. The fix is lossy: the original surrogate value is gone. Better still, avoid creating them: iterate by code point or grapheme instead of slicing by unit.