MALFORMED_QUERY: unexpected token
The SOQL parser couldn't make sense of your query. The error message tells you the exact word it choked on — that's where the syntax broke. Most often it's a quoting problem, an unsupported keyword, or a relationship name typo.
Also seen asMALFORMED_QUERY·unexpected token·MALFORMED_QUERY: unexpected token·INVALID_QUERY_LOCATOR malformed
A developer is testing a dynamic SOQL helper in the Developer Console. The query string looks fine when printed. Running it returns MALFORMED_QUERY: unexpected token: 'Industry' and the line of caret characters under the parser output points at the middle of the query. Nothing looks wrong. The same query, copied into the Query Editor and run manually, works.
What the parser is actually doing
SOQL is parsed by a strict, position-sensitive parser on the Salesforce side. Every query has to match a grammar that looks roughly like SELECT field-list FROM object [WHERE ...] [WITH ...] [GROUP BY ...] [ORDER BY ...] [LIMIT ...] [OFFSET ...] [FOR ...]. When the parser encounters a token in a position the grammar doesn't allow, it stops and returns MALFORMED_QUERY: unexpected token: '<token>'. The token in the message is the one that failed to match.
The message tells you what the parser saw. It doesn't tell you what the parser expected. That's the gotcha: the unexpected token is usually correct on its own, but it's in the wrong place, or what came before it is wrong, or what came after.
The parser is unforgiving in three specific ways. It enforces clause order strictly (you cannot put ORDER BY before WHERE). It rejects reserved keywords used as identifiers without backticks (Salesforce SOQL doesn't actually support backtick-quoting, so using a reserved word as an alias fails outright). And it doesn't recover from earlier syntax errors; the first failure stops the parse and you only see the first bad token.
The broken example
An Apex method that builds a query from user-supplied filter input:
public static List<Account> findAccounts(String industryFilter, String stageFilter) {
String query = 'SELECT Id, Name, Industry FROM Account ';
if (industryFilter != null) {
query += 'WHERE Industry = ' + industryFilter + ' ';
}
if (stageFilter != null) {
query += 'AND Owner.Profile.Name = ' + stageFilter + ' ';
}
query += 'ORDER BY Name';
return Database.query(query);
}
The author tested it with findAccounts(null, null) and it returned all accounts ordered by name. Passing findAccounts('Technology', null) returned a MALFORMED_QUERY: unexpected token: 'Technology'. Passing findAccounts(null, 'System Administrator') returned MALFORMED_QUERY: unexpected token: 'AND'.
Both failures share an underlying defect, but they look like different bugs because the parser stops at a different token in each case.
In the first call, the resulting string is SELECT Id, Name, Industry FROM Account WHERE Industry = Technology ORDER BY Name. The value Technology should be wrapped in quotes because it's a string literal. Without quotes, the parser tries to read it as a field name. There is no Technology field on Account. The parser flags the token.
In the second call, the string is SELECT Id, Name, Industry FROM Account AND Owner.Profile.Name = System Administrator ORDER BY Name. The WHERE was skipped because industryFilter was null, but the AND was appended anyway. The parser sees AND immediately after Account, which isn't a valid clause boundary, and fails.
Why string-concatenation SOQL is fragile
String concatenation makes the query author responsible for three things the SOQL grammar doesn't tolerate sloppiness on: quoting literals, escaping single quotes inside literals, and maintaining clause order across all branches of the conditional logic. Miss any one and the parser rejects the result.
Quoting is mechanical. Every string literal needs single quotes: Industry = 'Technology'. Numeric literals don't: Amount > 1000. Dates use a specific format: CreatedDate > 2026-01-01T00:00:00Z with no quotes. Boolean literals don't take quotes either: IsActive = true.
Escaping is required when a literal contains a single quote. The string O'Brien becomes 'O\'Brien' inside SOQL. If a user-supplied string contains a single quote and you concatenate it raw, the parser sees an unbalanced quote and either fails with MALFORMED_QUERY or, worse, executes a SOQL injection attack.
Clause-order tracking gets harder as the query has more optional fragments. Every combination has to produce valid SOQL. Manual concatenation makes this an exponential testing problem.
The fix: use bind variables, not concatenation
Apex SOQL supports bind variables. Inline a variable into a query with the :variableName syntax and the platform handles quoting, escaping, and type conversion automatically:
public static List<Account> findAccounts(String industryFilter, String profileFilter) {
if (industryFilter == null && profileFilter == null) {
return [SELECT Id, Name, Industry FROM Account ORDER BY Name];
}
if (industryFilter != null && profileFilter == null) {
return [
SELECT Id, Name, Industry FROM Account
WHERE Industry = :industryFilter
ORDER BY Name
];
}
if (industryFilter == null && profileFilter != null) {
return [
SELECT Id, Name, Industry FROM Account
WHERE Owner.Profile.Name = :profileFilter
ORDER BY Name
];
}
return [
SELECT Id, Name, Industry FROM Account
WHERE Industry = :industryFilter AND Owner.Profile.Name = :profileFilter
ORDER BY Name
];
}
Each branch produces a valid query because each query is statically typed. The compiler verifies the field names at compile time. The bind syntax handles the literal-quoting and quote-escaping at runtime. No string concatenation, no malformed queries.
The verbosity is real but worth it. Static SOQL is easier to read, easier to test, and impossible to inject.
When dynamic SOQL is genuinely needed
Some legitimate cases force dynamic SOQL. A list view that lets users choose any field to filter. A search component that targets one of many objects depending on context. A query builder where the column list comes from configuration.
For those cases, use Database.queryWithBinds and pass bind values in a typed map:
String objectName = 'Account';
String fieldName = 'Industry';
String valueToMatch = 'Technology';
String query =
'SELECT Id, Name FROM ' + objectName +
' WHERE ' + fieldName + ' = :v';
Map<String, Object> binds = new Map<String, Object>{ 'v' => valueToMatch };
List<SObject> rows = Database.queryWithBinds(query, binds, AccessLevel.USER_MODE);
The object and field names are still concatenated (they're identifiers, not values), but the literal value is bound. The parser never sees the value as part of the SQL text; the platform substitutes it after parsing. Quote-escaping is automatic. SOQL injection is impossible for the value itself.
The remaining risk: an attacker who controls objectName or fieldName could still inject identifier-level SOQL. Allowlist those inputs against a known set before concatenating.
The fixed example, end to end
A safe, dynamic version of the original helper:
public class AccountFinder {
private static final Set<String> ALLOWED_FIELDS = new Set<String>{
'Industry', 'Type', 'BillingCountry'
};
public static List<Account> findAccounts(String filterField, String filterValue) {
if (!ALLOWED_FIELDS.contains(filterField)) {
throw new IllegalArgumentException(
'Invalid filter field: ' + filterField
);
}
String query =
'SELECT Id, Name, Industry FROM Account ' +
'WHERE ' + filterField + ' = :v ' +
'ORDER BY Name LIMIT 200';
Map<String, Object> binds = new Map<String, Object>{ 'v' => filterValue };
return (List<Account>) Database.queryWithBinds(
query, binds, AccessLevel.USER_MODE
);
}
}
The field name is allowlisted, the value is bound, the query is well-formed for every input, and the result is bounded by a LIMIT so a misuse can't burn the heap.
Reading the message itself
When the parser returns MALFORMED_QUERY: unexpected token: '<X>', the right reading strategy is:
- Print the exact query string being sent (with
System.debug(query)or by logging it on the server). - Find
<X>in the printed string. - Read the tokens before and after it. The actual error is almost always immediately to the left.
A common pattern: the unexpected token is AND or OR, and the cause is that the preceding clause boundary is wrong. The fragment of the query just before the failed token tells you what's missing or mis-ordered.
A second pattern: the unexpected token is a field name, and the cause is missing quotes around what should have been a string literal earlier in the query.
A third pattern: the unexpected token is SELECT or FROM deep in the query, and the cause is a subquery that wasn't properly parenthesized.
Reserved words and field aliases
The SOQL grammar reserves certain words. Using them as field aliases or column names without escaping fails:
SELECT Name, Owner FROM Account
Owner is a relationship name (not a reserved word in the strictest sense), so this query is actually fine. But aliasing a column with a reserved word fails:
SELECT Name, Industry FROM Account GROUP BY Industry HAVING COUNT(Id) > 5
That's also fine. The interesting failure is:
SELECT Name name FROM Account
name is not a reserved word here either. The most common reserved-word trap is using Date or Datetime as an alias, which fails. The fix is to choose a different alias.
Common shapes that fire this error
- Forgetting quotes around a string literal:
WHERE Name = Acmeinstead ofWHERE Name = 'Acme'. - Forgetting to remove a trailing
ANDafter dropping a conditional filter:WHERE Industry = 'Tech' AND ORDER BY Name. - Putting clauses in the wrong order:
ORDER BY Name WHERE Industry = 'Tech'. - Mismatched parentheses inside a subquery:
(SELECT Id FROM Contactsmissing the closing paren. - Using SQL syntax that SOQL doesn't support:
INNER JOIN,LEFT OUTER JOIN,UNION,DISTINCToutside aggregate functions, table aliases likeAccount a. - Date literal in the wrong format:
WHERE CreatedDate > '2026-01-01'(the quotes make it a string, not a date). - Picklist or boolean value with quotes when none are needed:
WHERE IsActive = 'true'instead ofWHERE IsActive = true.
Each of these shows up as MALFORMED_QUERY: unexpected token: '<X>' where <X> is whatever the parser hit first.
Testing dynamic queries
For any code that builds dynamic SOQL, write unit tests that exercise the failure paths along with the happy path:
@isTest
static void findAccounts_rejectsUnknownField() {
try {
AccountFinder.findAccounts('SecretField__c', 'value');
System.assert(false, 'Expected IllegalArgumentException');
} catch (IllegalArgumentException ex) {
System.assert(ex.getMessage().contains('Invalid filter field'));
}
}
@isTest
static void findAccounts_buildsValidQueryForKnownField() {
insert new Account(Name = 'Acme', Industry = 'Technology');
Test.startTest();
List<Account> results = AccountFinder.findAccounts('Industry', 'Technology');
Test.stopTest();
System.assertEquals(1, results.size());
}
The first test confirms the allowlist works. The second confirms the query parses and runs. Together they catch the most common ways the helper can fail.
Closely related errors
| Error | Cause |
|---|---|
MALFORMED_QUERY: unexpected token: '<X>' | Token in wrong position or missing quotes around a literal |
MALFORMED_QUERY: Variable does not exist: <name> | Bind variable referenced in SOQL doesn't exist in scope |
INVALID_FIELD: No such column '<X>' on entity '<Y>' | Field name typo or field doesn't exist on the object |
INVALID_TYPE: sObject type '<X>' is not supported | Object name typo or object not visible to running user |
QueryException: Non-selective query | Filter not selective enough; missing index on the filter field |
All five live in the SOQL parser's family of validation failures. The fixes overlap: print the query, read the message, fix the named token.
Defensive habits
Prefer static SOQL whenever possible. The compiler validates field and object names at compile time, which prevents the worst class of typos.
When you must use dynamic SOQL, allowlist identifiers (object and field names) against a known set and bind every literal value. Never concatenate user input directly into a query.
Log the exact query string before sending it. The parser's message points at a position in the query that's only meaningful if you can see the full string.
Test edge cases: empty inputs, special characters in values, all combinations of optional filters. The exponential testing burden of string-concatenation SOQL is exactly why the bind-variable approach pays off.
Workbench and the Query Editor as debugging tools
When a dynamic query fails in Apex, the fastest sanity check is to paste the printed query into the Developer Console's Query Editor or into Workbench. Both run against your org's metadata, both surface the parser message inline, and both let you edit the query character by character until it parses. The Query Editor shows the result count and a preview of rows, which lets you confirm the query does what you expected once the syntax is valid.
A useful trick: when the parser flags a token deep in the query, delete clauses one at a time from the end of the query until it parses. The clause you removed last is usually the broken one. Reverse the process to identify the exact missing or mis-ordered piece.
For complex queries with multiple subqueries or relationship traversals, format the query across multiple lines before pasting. The parser doesn't care about whitespace, and reading the query as indented lines makes the broken position obvious in a way the single-line string never does.
Further reading from Salesforce
Related dictionary terms
Share this fix
Related SOQL errors
INVALID_FIELD: <field> is not filterable
SOQLYou used a field in `WHERE`, `ORDER BY`, or `GROUP BY` that the platform refuses to filter or sort on — almost always a Long Text Area, Rich…
INVALID_FIELD: No such column 'X' on entity 'Y'
SOQLEither the field truly doesn't exist on the object, or it exists but the running user / API session can't see it. The same error message is …
INVALID_FIELD: NULL_FOR_NON_REFERENCE_FIELD: <field> can not be null
SOQLYou assigned `null` to a field type that doesn't accept null — usually a Boolean (which expects `true` or `false`, not null) or a primitive …
INVALID_SEARCH: search term must be longer than one character
SOQLA SOSL `FIND` expression had a search term shorter than two characters, or used wildcards in a way the parser rejects. SOSL has stricter rul…
OPERATION_TOO_LARGE: Aggregate query has too many rows for direct assignment, use FOR loop
SOQLAn aggregate or relationship query returned more rows than Apex will assign to a `List<AggregateResult>` variable in one go. The platform te…