System.TypeException: Invalid id: <value>
You cast a String to `Id` but the string isn't a syntactically valid Salesforce ID — wrong length, wrong characters, or just a typo. Different from `INVALID_CROSS_REFERENCE_KEY`, which fires when the ID looks valid but doesn't exist or is the wrong sObject.
Also seen asTypeException: Invalid id·Invalid id apex·System.TypeException: Invalid id
The Apex Id type isn't a free-form string. The runtime validates the format on assignment: 15 or 18 alphanumeric characters, no hyphens, no spaces. Fail validation, you get TypeException, before any DB lookup happens.
Id valid = '0016x00000ABCDE'; // OK — 15 chars
Id valid18 = '0016x00000ABCDEAA1'; // OK — 18 chars (with checksum)
Id bad = '0016x00000ABCD'; // ❌ TypeException — 14 chars
Id bad2 = 'foo bar'; // ❌ TypeException — invalid chars
Id bad3 = '00-16x000-00ABCDE'; // ❌ TypeException — has hyphens
The cast happens implicitly any time you assign to an Id variable, pass to a method expecting Id, or use : in SOQL with an Id-typed bind.
Where it usually comes from
- A Lightning component passing a record ID that wasn't trimmed, normalised, or properly URL-decoded.
recordIdfrom the URL is sometimes%2F-encoded, or has a trailing space. - A custom-button URL parameter copied from the address bar where Salesforce included extra path segments.
- A Process Builder / Flow passing a string that isn't an ID at all (e.g., a custom field value).
- External-system payloads where the upstream system uses its own ID format and someone confused that with the Salesforce ID.
How to validate before casting
public static Boolean isValidId(String s) {
if (s == null) return false;
if (s.length() != 15 && s.length() != 18) return false;
return Pattern.matches('^[a-zA-Z0-9]+$', s);
}
if (!isValidId(input)) {
throw new IllegalArgumentException('Not a valid Salesforce Id: ' + input);
}
Id parsed = (Id) input;
Or use Schema.SObjectType to also confirm the prefix matches the sObject you expect:
public static Boolean isAccountId(String s) {
if (!isValidId(s)) return false;
Id parsed = (Id) s;
return parsed.getSObjectType() == Account.SObjectType;
}
This rejects 005xxx... (User) when you wanted 001xxx... (Account), before you try to query.
A subtle case: 15 vs 18
15-character IDs are case-sensitive (the same record is 0016x00000ABCDE only — 0016X00000ABCDE is technically a different ID in the 15-char namespace). 18-character IDs are case-insensitive — the trailing 3 characters encode the case.
If your code uppercases or lowercases an ID before storing it, switching to 18-char form first gets you safe round-tripping:
String id18 = ((Id) input15).to18(); // .to18() is on Id, not String
