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
A REST callback hands you a record id as a string. Your code casts it to Id and the platform throws System.TypeException: Invalid id: 001xx000003DGb0. The string looks like a Salesforce id. It's even the right length. The platform refuses anyway. Twenty minutes later you discover the partner system has been generating ids that pass a length check but aren't valid Salesforce identifiers.
What the platform validates when you cast to Id
Salesforce ids have a specific shape:
- 15 characters or 18 characters. The 15-character form is case-sensitive; the 18-character form has a 3-character checksum appended that makes it case-insensitive.
- Alphanumeric only, base 62. Characters 0-9, A-Z, and a-z. No special characters, no spaces, no underscores.
- The first three characters are the SObject key prefix. Every object type has a fixed prefix:
001for Account,003for Contact,006for Opportunity, and so on. The platform validates that the prefix corresponds to a known object.
When you cast a string to Id (either via the Id keyword or by assigning to an Id variable), Apex runs these checks. If any fail, you get TypeException: Invalid id. The exception message includes the offending value so you can see exactly what the platform saw.
This is different from INVALID_CROSS_REFERENCE_KEY, which fires when the id has valid shape but doesn't correspond to a real record (record deleted, wrong object type for the field, or never existed). TypeException is purely a syntactic check; INVALID_CROSS_REFERENCE_KEY is a semantic one.
The broken example
A bulk callback handler that parses ids from a partner system:
public class PartnerCallbackHandler {
public static void processCallbacks(List<String> rawIds) {
Set<Id> accountIds = new Set<Id>();
for (String raw : rawIds) {
accountIds.add((Id) raw); // Throws on the first malformed id
}
for (Account a : [SELECT Id, Name FROM Account WHERE Id IN :accountIds]) {
// Process...
}
}
}
The partner sends a batch of 50 ids. 49 are valid; one is 001-x000-3DGb0 (with hyphens). The cast on that one throws and the entire batch fails. The other 49 valid ids never get processed.
The fix: validate before casting
Apex provides Id.valueOf(string), which throws the same exception on invalid input but is easier to wrap in a try/catch for per-id error handling. Alternatively, you can validate the string format yourself with a regex before casting.
private static final Pattern VALID_ID = Pattern.compile('^[a-zA-Z0-9]{15}([a-zA-Z0-9]{3})?$');
public static void processCallbacks(List<String> rawIds) {
Set<Id> validIds = new Set<Id>();
List<String> invalidIds = new List<String>();
for (String raw : rawIds) {
if (String.isBlank(raw)) continue;
if (!VALID_ID.matcher(raw).matches()) {
invalidIds.add(raw);
continue;
}
try {
validIds.add((Id) raw);
} catch (System.TypeException ex) {
// Regex passed but Apex rejected (rare; usually unknown prefix).
invalidIds.add(raw);
}
}
if (!invalidIds.isEmpty()) {
System.debug(LoggingLevel.WARN, 'Skipped invalid ids: ' + invalidIds);
// Optionally log to a custom object for partner-system feedback.
}
if (!validIds.isEmpty()) {
for (Account a : [SELECT Id, Name FROM Account WHERE Id IN :validIds]) {
// Process...
}
}
}
The pattern lets the valid ids continue processing while the invalid ones get logged for follow-up. The integration is more resilient.
The fixed example, with structured error reporting
A complete handler that returns structured per-id results:
public class PartnerCallbackHandler {
public class Result {
@AuraEnabled public String inputId;
@AuraEnabled public Boolean success;
@AuraEnabled public String message;
}
private static final Pattern VALID_ID = Pattern.compile('^[a-zA-Z0-9]{15}([a-zA-Z0-9]{3})?$');
public static List<Result> processCallbacks(List<String> rawIds) {
List<Result> results = new List<Result>();
Set<Id> validIds = new Set<Id>();
for (String raw : rawIds) {
Result r = new Result();
r.inputId = raw;
if (String.isBlank(raw)) {
r.success = false;
r.message = 'Empty id';
results.add(r);
continue;
}
if (!VALID_ID.matcher(raw).matches()) {
r.success = false;
r.message = 'Malformed id (wrong length or characters)';
results.add(r);
continue;
}
try {
Id parsed = (Id) raw;
if (parsed.getSObjectType() != Account.SObjectType) {
r.success = false;
r.message = 'Id is not an Account';
results.add(r);
continue;
}
validIds.add(parsed);
r.success = true;
r.message = 'Queued for processing';
results.add(r);
} catch (System.TypeException ex) {
r.success = false;
r.message = 'Apex rejected: ' + ex.getMessage();
results.add(r);
}
}
if (!validIds.isEmpty()) {
Map<Id, Account> existing = new Map<Id, Account>(
[SELECT Id FROM Account WHERE Id IN :validIds]
);
for (Result r : results) {
if (!r.success) continue;
if (!existing.containsKey((Id) r.inputId)) {
r.success = false;
r.message = 'Account not found (deleted or never existed)';
}
}
}
return results;
}
}
The handler returns a per-id outcome. The partner system gets a clear answer for each id: succeeded, was malformed, was the wrong type, or didn't exist. Each failure mode is distinguishable.
The SObject type check
The line parsed.getSObjectType() != Account.SObjectType catches a subtle bug: an id that's structurally valid but points to a different object. If the partner accidentally sends Opportunity ids labeled as Account ids, the cast itself succeeds (both Account and Opportunity ids are valid Salesforce ids), but the downstream code that assumes Account behavior fails.
getSObjectType() extracts the object type from the id's three-character prefix and lets you verify it matches expectation. The check is one line; the diagnostic value is enormous.
A reference for the most common prefixes:
| Prefix | Object |
|---|---|
| 001 | Account |
| 003 | Contact |
| 006 | Opportunity |
| 00Q | Lead |
| 500 | Case |
| 005 | User |
| 00e | Profile |
| 00G | Group |
| 0F9 | Network (Community) |
Custom objects get prefixes assigned at creation time, typically starting with a followed by two random characters. The platform's reference page lists every standard prefix.
A subtle source: copy-paste with surrounding text
Users copy-paste record ids from the URL bar, but URLs include other parts: https://acme.lightning.force.com/lightning/r/Account/001xx000003DGb0/view. If the copy includes the leading slash or trailing /view, the cast fails. Validation against the regex catches this.
Some Apex code receives ids via input parameters that arrive with whitespace or quotes from a CSV upload. Trim the string before casting: (Id) raw.trim(). The trim handles the most common copy-paste issues.
15 vs 18 character ids
Both lengths are valid. Apex normalizes internally; once you have an Id value, it's treated the same regardless of the input length. The 3-character suffix in the 18-character form is a checksum that lets case-insensitive systems (like older databases) safely transmit the id.
When sending ids to external systems, send the 18-character form to avoid case-sensitivity issues. The 18-character form is what String.valueOf(record.Id) returns. When receiving from external systems, accept either form.
The regex above ({15}([a-zA-Z0-9]{3})?) allows both lengths.
Why the platform throws instead of returning null
Apex's design pattern for malformed input is to throw, not to silently produce a sentinel. The advantage is that the bug is visible at the call site rather than several methods later when a null id flows into a SOQL query. The disadvantage is that unhandled cast errors stop the transaction.
The handle-and-log pattern shown above (catch the exception, log the bad id, continue with the valid ones) is the standard mitigation. Apply it any time you're parsing ids from an external source.
The interaction with Id.valueOf
Id.valueOf(String) is the explicit conversion method. It's equivalent to the (Id) str cast in terms of validation, but easier to wrap:
Id parsed;
try {
parsed = Id.valueOf(raw);
} catch (StringException | TypeException ex) {
// Both exceptions can fire depending on the input shape.
}
The double-exception catch is occasionally needed: StringException for completely malformed input, TypeException for input that's the right length but has an invalid object prefix.
For new code, prefer Id.valueOf over (Id) str for two reasons: the method name signals intent more clearly, and it's marginally easier to mock in tests.
A common diagnostic mistake
When users see TypeException: Invalid id and the id "looks right," they sometimes conclude the platform is buggy. It almost never is. Walk this list:
- Check the length. 15 or 18, no more, no less.
- Check the characters. Alphanumeric only, no hyphens, dots, or underscores.
- Check the prefix. Does it match a known object? Open Setup → Object Manager → object → API Name and confirm.
- Check for whitespace. Trim the string before validation.
- Check for encoding. If the id came from URL encoding, decode first.
After all five checks pass, the id is almost certainly valid. If the cast still fails, it's worth filing a support case, but in practice every case I've seen turned out to be one of the five.
Where the malformed ids tend to come from
Three sources produce most invalid-id incidents.
Hand-typed ids. A user pastes an id into a custom UI field with a slight typo. The integration tries to process it and fails. The fix is upstream: validate at the input field with a regex pattern that matches Salesforce id syntax. Show a useful error message if the format is wrong.
Older integrations that strip the suffix. Some integrations were written to send 15-character ids but the source code adds extra characters (spaces, line breaks). Trim and length-check before casting.
Concatenation bugs. Code that builds an id from parts ('001' + accountSuffix) sometimes produces invalid lengths or extra characters. Build a unit test that exercises each construction path with edge-case inputs.
For each source, the fix lives upstream of the cast. Logging the offending value (as the example handler does) lets you find the upstream bug instead of just patching the symptom.
How tests should exercise this
A test suite for ID-handling code should include:
- A happy-path test with a valid Account id from the seed data.
- A malformed-length test with a 14-character or 19-character string.
- A non-alphanumeric test with a string containing dashes or spaces.
- A wrong-object-type test with a valid Contact id passed to Account-only code.
- A non-existent test with a syntactically valid id that doesn't correspond to a real record.
The fifth case requires a real Apex id that you generate but don't insert. The Id.valueOf of any 15-or-18-character alphanumeric string with a valid prefix produces a syntactically valid id, even if no record exists.
Five tests cover the full failure space. Each runs in milliseconds. The overall suite catches every flavor of invalid-id bug before it ships.
The performance side
Casting strings to ids has measurable cost. The platform's id validation runs character-by-character, checks the prefix against the platform's object catalog, and (for 18-character ids) recomputes the checksum to confirm the cast. In a hot loop processing hundreds of thousands of ids, the cumulative time adds up.
For high-volume id processing, two optimizations help:
Pre-filter with a fast regex. A regex match is faster than the full id cast. Filter out obviously-malformed input before the cast.
Batch by SObject type. If you know all ids should be Accounts, accept that assumption and validate with a single startsWith check on the 001 prefix. The check is far faster than calling getSObjectType() per id.
These optimizations rarely matter for typical workloads. Apply them only when profiling shows id validation as a bottleneck.
A note on the dynamic API
Calls into the dynamic Apex API (like Database.query(query) or JSON.deserializeUntyped) often hand you ids as raw strings or as untyped Object values. The cast to Id still validates; the same TypeException can fire. Treat any id from a dynamic source with the same suspicion as one from an external system: validate before casting.
Further reading from Salesforce
Related dictionary terms
Share this fix
Related Apex errors
Initial term of field expression must be a concrete SObject: <type>
ApexYou wrote `someThing.Field__c` where `someThing` isn't a specific SObject type — usually an `SObject` or `Object` reference. Apex needs the …
Method does not exist or incorrect signature
ApexThe Apex compiler can't find a method matching the call you wrote — wrong name, wrong argument types, or wrong number of arguments. The comp…
MIXED_DML_OPERATION: DML operation on setup object is not allowed after you have updated a non-setup object (or vice versa)
ApexSalesforce splits objects into "setup" (User, Group, GroupMember, PermissionSet, Profile, Queue, etc.) and "non-setup" (everything else). A …
System.AsyncException: Future method cannot be called from a future or batch method
ApexYou called an `@future` method from inside another `@future`, batch, or queueable. Salesforce blocks recursive async chains because they cou…
System.CalloutException: Read timed out
ApexThe HTTP callout exceeded its allowed read time waiting for the remote server's response. Salesforce caps a single callout at 120 seconds (d…