graphemely

What is a lone surrogate and how do I remove it?

A lone surrogate is half of an emoji (or any character outside the Basic Multilingual Plane) left behind when a string is cut by code unit. Detect it with isWellFormed(str) and replace it with U+FFFD using toWellFormed(str).

The fix, copy and paste

example.js
import { isWellFormed, toWellFormed } from '@kwy404/graphemely';

const broken = '\u{1F600}'.slice(0, 1);   // '\uD83D'
isWellFormed(broken);                    // false
toWellFormed('ok' + broken);             // 'ok\u{FFFD}'
encodeURIComponent(toWellFormed(broken)); // '%EF%BF%BD', no URIError

Emoji in the code samples are written as JavaScript escapes, for example \u{1F468} for a man. In your own code you can type the emoji directly.

Why lone surrogates are a problem

Where they come from

Almost always from cutting text with code unit indices: str.slice(0, n), str.substring, str[i], str.split('') or a maximum length enforced by an input field. The real fix is to cut by grapheme cluster with truncate or slice, so they never appear. toWellFormed is the safety net for data that is already broken.

Native methods

ES2024 added String.prototype.isWellFormed and toWellFormed. They are missing on Node 18 and older browsers. The graphemely versions behave the same and work everywhere.

Install

Terminal
npm install @kwy404/graphemely

Works in Node 16 and newer, all modern browsers, Deno and Bun. ESM and CommonJS, TypeScript types included, zero dependencies, about 2 kB gzipped. Try your own text in the playground.

FAQ

A UTF-16 code unit in the range U+D800 to U+DFFF that is not part of a valid high and low surrogate pair. It is half of a character.

Call isWellFormed(str) from graphemely, or str.isWellFormed() on runtimes that support ES2024.

Call toWellFormed(str), which replaces each one with U+FFFD, the replacement character. Then fix the code that cut the string by code unit.

Related guides