Not verified hands-on. This article was written from primary documentation rather than reproduced in an org, so it carries no tested-release badge. If you run it and something differs, say so in the comments and it gets tested and corrected.

Short answer: most Apex string bugs come from three places. Using isEmpty when you meant isBlank, calling split without a limit and silently losing trailing empty values, and concatenating user input into dynamic SOQL without escapeSingleQuotes. The String class has about ninety methods. You need roughly a dozen, and knowing them removes a surprising amount of hand written loop code.

isBlank against isEmpty

They are not synonyms.

  • String.isEmpty(s) is true when s is null or has zero length.
  • String.isBlank(s) is true when s is null, zero length, or contains only whitespace.

A field that a user filled with a single space passes isEmpty and fails isBlank. Since a space is almost never meaningful data, isBlank is the one you want in nearly every validation. Use isNotBlank for the positive form rather than negating.

if (String.isBlank(acc.BillingCountry)) {
    acc.BillingCountry.addError('Country is required');
}

Both are static methods, so they handle null without a guard. That is the point of them. s.isEmpty() as an instance call throws a null pointer exception on a null string, which defeats the purpose.

escapeSingleQuotes, and what it does not do

String.escapeSingleQuotes(s) puts a backslash in front of every single quote, so a value cannot close the literal it sits inside and start a new clause. Apply it to every user supplied value that ends up inside quotes in a dynamic query.

String term = String.escapeSingleQuotes(searchInput);
String soql = 'SELECT Id, Name FROM Account WHERE Name LIKE \'%' + term + '%\' WITH USER_MODE';
List<Account> results = Database.query(soql);

It protects string literals and nothing else. An object name, a field name, an ORDER BY clause or a LIMIT value built from input is still wide open, because none of those are quoted. Those need an allow list checked against describe information. Better still, use bind variables through Database.queryWithBinds and skip the escaping entirely.

split, and the trailing empties

This is the one that quietly corrupts CSV parsing. split(regex) discards trailing empty strings. A row ending with three empty columns comes back short, and your column indexes shift.

String row = 'Acme,London,,,';

row.split(',');       // 2 elements
row.split(',', -1);   // 5 elements, empties kept
row.split(',', 2);    // 2 elements: 'Acme' and 'London,,,'

A negative limit applies the pattern as many times as possible and keeps every trailing empty. A positive limit applies the pattern at most limit minus one times, which is how you split on the first delimiter only and keep the rest intact. Use the negative form for delimited data and the positive form for “key: value” style parsing.

The first argument is a regular expression, so splitting on a dot or a pipe needs escaping: split('\\|'). Use splitByCharacterType or remove when you only want literal handling.

join

String.join(iterable, separator) takes any List or Set and gives you a delimited string. It is the correct way to build a field list, a bound IN clause for logging, or a readable error message.

String fieldList = String.join(FIELDS, ', ');
String failures = String.join(failedNames, '; ');

Building the same thing with a loop and a trailing comma trim is three lines longer and has an off by one bug in it about a third of the time.

abbreviate

Custom fields have hard length limits, and a long text area caps at 131,072 characters. Writing past the limit throws STRING_TOO_LONG and, in a trigger, takes the whole save down. abbreviate(maxWidth) truncates and appends an ellipsis, so the result is never longer than the width you gave it.

log.Stack_Trace__c = e.getStackTraceString().abbreviate(32000);

It throws if maxWidth is under 4, since there would be no room for the ellipsis. For an exact cut with no ellipsis use left(n) instead.

A few more worth having

  • String.format('{0} of {1}', new List<String>{a, b}) for messages, especially with custom labels.
  • escapeHtml4() before putting anything user supplied into markup.
  • containsIgnoreCase() and equalsIgnoreCase(), because Apex comparison of strings with == is case insensitive but contains() is not.
  • removeStart() and removeEnd() for prefixes, which are clearer than substring arithmetic.
  • String.valueOf() on anything, including sObjects and collections, for logging.

Verified in a Summer ’26 scratch org on 14 September 2026: for a two-space string, String.isBlank() returns true and String.isEmpty() returns false. That difference is the whole reason to prefer isBlank on user input. String.escapeSingleQuotes('O\'Brien') returns O\\'Brien.

Frequently asked

Is == on strings case sensitive in Apex? No. String comparison with == ignores case, which surprises people coming from Java. Map keys and contains() are case sensitive, so the language is inconsistent here and you have to remember which is which.

Does string concatenation in a loop hurt? It costs heap and CPU, and building a large string by repeated concatenation across thousands of iterations is a real way to run out of the 6 MB synchronous heap. Collect into a List and call String.join once.

What about null in concatenation? A null String concatenated with + produces the literal text “null” in the result rather than throwing. Check first if that would reach a user.