Anchor Error Code Collision — Same Number, Different Meaning
The transaction fails with error 6002. I know this number. I've decoded it before. I open the program's error enum, count to index two, and read the variant name. It describes an invalid parameter condition. I spend twenty minutes inspecting every parameter in my instruction, triple-checking values, comparing against working transactions. Everything looks correct. The parameters match. The accounts are right. The data is properly formatted. But the transaction keeps failing with 6002.
After an hour, I realize I'm looking at the wrong program's error enum.
The 6002 isn't coming from the program I think it's coming from. It's coming from a different program — one that the first program calls internally via CPI. That other program also has an error enum. Its index-two variant means something completely different. The number is identical. The meaning is unrelated. I've been debugging the wrong problem for an hour because I assumed the number belonged to one program when it belongs to another.
This is the moment I understand something fundamental about on-chain error codes: an error code is not a number. It's a tuple — a (program, number) pair. Without knowing which program produced the number, the number is meaningless. Worse than meaningless — it's actively misleading, because it looks like information when it's actually ambiguity.
The Numbering System That Creates Collisions
The Anchor framework, which most Solana programs are built on, divides its error space into two zones. Framework errors — things like account deserialization failures, constraint violations, and ownership checks — occupy the range below 6000. These are generic. They mean the same thing regardless of which program produces them. "Account not initialized" is "account not initialized" whether it comes from a DEX, a lending protocol, or a staking program.
Custom errors start at 6000. Each program defines its own error enum — a list of conditions specific to that program's business logic. The first variant in the enum becomes error 6000. The second becomes 6001. The third becomes 6002. And so on.
This is where collisions happen. Every program that uses Anchor starts its custom errors at 6000. Every program's first custom error is 6000. Every program's third custom error is 6002. The numbers aren't globally unique. They're program-local. Error 6002 in Program A and error 6002 in Program B are completely unrelated conditions that happen to share a number, the way area code 212 means Manhattan in the phone system but would mean nothing — or something entirely different — in a different country's numbering scheme. The three digits are the same. The system that assigns meaning to those digits is different.
Phone numbers work because you always know the country and area code before you dial. You don't just see "555-1234" floating in space and try to figure out which city it belongs to. The full context — country code, area code, number — is always present together. Error codes on Solana don't always arrive with that context attached. Sometimes the logs make it clear which program emitted the error. Sometimes they don't.
How CPI Creates the Collision
A simple transaction calls one program. The program either succeeds or fails. If it fails with error 6002, the source is unambiguous — there's only one program in the picture. Look up 6002 in that program's error enum. Done.
But most interesting transactions on Solana aren't simple. They involve Cross-Program Invocations — CPI — where one program calls another. My swap instruction calls DEX Program A. DEX Program A internally calls the Token Program to move tokens. It might call an Oracle Program to update a price feed. It might call a Fee Program to collect protocol fees. Each of these is a separate program with its own error definitions.
When the transaction fails with error 6002, it could be coming from any program in that CPI chain. Did DEX Program A fail because of its own custom error at index two? Or did DEX Program A successfully execute its logic, call the Token Program via CPI, and the Token Program rejected the operation for its own reason that also happens to be encoded as 6002? Or did the CPI go deeper — DEX Program A calls an intermediate program, which calls another program, and the error bubbled up through two layers of CPI before reaching my transaction logs?
The error code travels up the CPI chain like a message passed through a game of telephone. The number arrives intact, but the attribution gets lost. By the time I see "Custom Error: 6002" in my transaction's failed result, the number has been stripped of its most important piece of context: who said it.
This is like receiving a court case number — say, case number 2024-CV-1234 — without knowing which court it was filed in. In the Los Angeles Superior Court, that case number refers to one dispute. In the Cook County Circuit Court, the same format might refer to a completely different case. The number is a local identifier within a specific jurisdiction. Without the jurisdiction, the number is just digits.
Half a Day on One Number
The debugging session that teaches me this lesson costs half a day.
The bot identifies a profitable cycle. The cycle involves three hops across two different DEX programs. The transaction fails with error 6002. I've seen 6002 before from the first DEX program — it means an invalid pool state condition in that program's error enum. I've fixed this before by refreshing stale pool data before constructing the instruction. Standard fix.
I refresh the pool data. The transaction still fails with 6002. I add more aggressive state verification. Still 6002. I inspect every field in the pool state account, byte by byte, comparing against the raw on-chain data. Everything matches. The pool state is current. The parameters are correct. There is no invalid pool state condition. And yet: 6002.
This is the debugging equivalent of checking into a hotel, being told your room number is 412, going to floor four, and finding that room 412 is occupied by someone else. The number is correct. You're in the wrong building. The front desk gave you a room number without specifying which tower of the hotel complex you should go to, and you assumed the obvious one.
I'm in the wrong program.
The transaction involves CPI. The first DEX program calls a second program as part of its swap execution. That second program has its own error enum. Its error 6002 — same number, index two in its enum — means something completely different. It's not about pool state at all. It's about an account authority mismatch. A signer that the second program expects but doesn't find. An entirely different category of problem, with an entirely different fix, wearing the same numerical mask.
Once I identify the correct source program, the fix takes ten minutes. The problem is clear: a PDA authority that the inner program expects isn't being derived correctly. I fix the derivation, the inner CPI succeeds, the transaction lands. Ten minutes of fixing. Four hours of looking in the wrong place.
The Program ID Is the First Question
After this experience, I change my debugging protocol. When a transaction fails with a custom error code, the first question is no longer "what does this error code mean?" The first question is: "which program produced this error code?"
The answer lives in the transaction logs. Solana programs emit log messages during execution, and these logs include program invocation markers. When Program A starts executing, the logs show "Program A invoked." When Program A calls Program B via CPI, the logs show "Program B invoked." When Program B fails, the logs show "Program B failed" with the error code, followed by "Program A failed" as the error propagates up.
Reading these logs carefully — not skimming for the error code, but tracing the invocation chain — reveals which program actually produced the error. The error appears first at the deepest level of the CPI chain, next to the program that actually failed. Every subsequent appearance is just propagation — the caller failing because its callee failed.
It's the same principle behind reading a hospital bill. The bill might show a total of $3,400 with several line items. Each line item has a billing code — CPT codes for procedures, ICD codes for diagnoses, HCPCS codes for supplies. But the codes mean different things depending on the department. A code from the radiology department means one thing. The same code from the lab means another. The bill usually lists the department next to each charge, and reading the department is essential before looking up the code. Skip the department, and you might be researching what a radiology code means when the charge actually came from the pharmacy.
I start reading program IDs first, error codes second. Every time. The program ID tells me which error enum to consult. The error code tells me which entry in that enum. Both pieces are required. Either one alone is insufficient.
The IDL Makes or Breaks the Experience
Programs built with Anchor can publish an IDL — an Interface Definition Language file that describes the program's interface, including a complete mapping of error codes to human-readable names and descriptions. When an IDL is available, decoding an error is mechanical: load the IDL, find the error code, read the description. Some tools do this automatically, showing "Error 6002: InsufficientLiquidity" instead of just "Error 6002."
When the IDL is available for every program in the CPI chain, debugging becomes manageable. I identify which program failed, load that program's IDL, look up the error code, and get a description that at least points me in the right direction. The description might not be perfect — "InsufficientLiquidity" doesn't tell me which token is insufficient or by how much — but it's a category that narrows the search space dramatically.
When the IDL isn't available, I'm doing archaeology. I find the program's source code — if it's open source. I locate the error enum — usually a Rust file with an #[error_code] attribute. I count from the top of the enum to the index I need. And I hope that the source code I'm reading matches the version actually deployed on-chain, because if the developers added or removed error variants between the version I'm reading and the deployed version, my count is wrong, and I'm decoding a different error entirely.
This happens. Programs get updated. Error enums get modified. A new error variant gets inserted at position one, and suddenly everything that used to be at index two is now at index three. The deployed program's error 6002 no longer matches what the published source code says 6002 means. The code is stale. My lookup is wrong. And I don't know it's wrong because there's no runtime verification — nothing says "hey, you're reading the wrong version of this enum."
It's the equivalent of using last year's ZIP code directory to look up a newly redistricted area. The ZIP code exists. The directory has an entry for it. But the entry describes a different geographic region than the one the ZIP code currently refers to, because the boundaries changed. The directory doesn't warn you it's outdated. It just gives you the old answer with full confidence.
The Layered Ambiguity
The collision problem compounds when the CPI chain is deep. Consider a transaction where my instruction calls Program A, which calls Program B, which calls Program C. The transaction fails with error 6003. Three programs. Three separate error enums. Three possible meanings for 6003.
Program A's 6003 might mean "swap amount exceeds maximum." Program B's 6003 might mean "oracle price is stale." Program C's 6003 might mean "insufficient signer authority." These are not related conditions. They require different diagnostic approaches and different fixes. But they all look the same from the outside: "Custom Error: 6003."
And the CPI chain in Solana MEV transactions is often deeper than developers expect. A swap through a DEX might involve: the DEX program itself, the SPL Token program (or Token-2022), an oracle program, a fee collection program, and possibly an event-logging program. Each layer is a potential source of the error code. Each layer has its own error definitions.
The ambiguity isn't just theoretical. It's practical and frequent. In a single week of debugging, I encounter the same error code from three different programs, each time requiring a different fix. The first time, 6002 means an invalid account in the DEX program. The second time, 6002 means a token mint mismatch in a different DEX program. The third time, 6002 means an arithmetic overflow in yet another program that the DEX calls internally. Same number. Three different root causes. Three different programs. Three different weeks of debugging if I don't check the program ID first.
Building the Disambiguation Habit
The protocol I develop is simple but rigid. When a transaction fails with a custom error code:
First, pull the full transaction logs. Not the summary. Not the explorer's abbreviated view. The full logs, with every program invocation and every log message.
Second, trace the invocation chain. Find the deepest "Program failed" entry. That's the program that actually produced the error. Everything above it is propagation — programs failing because their callees failed.
Third, identify the failing program by its program ID. Not by what I expect it to be. Not by what program I called in my instruction. By the actual program ID in the logs.
Fourth, now — and only now — look up the error code in that specific program's error definitions. If the program has an IDL, use the IDL. If not, find the source code and count through the error enum.
This four-step process adds maybe thirty seconds to each debugging cycle. It saves hours of misdirected investigation. The discipline of checking the program ID before interpreting the error code is the single highest-leverage debugging habit I develop for on-chain work.
It's the difference between hearing a siren and immediately assuming it's a fire truck, versus looking out the window to check whether it's a fire truck, an ambulance, or a police car. The siren sounds similar. The response is very different. Looking first costs two seconds. Assuming wrong costs the time it takes to realize you called the fire department when you needed an ambulance.
The Tuple Insight
The lesson distills to one principle: an error code on Solana is not a number. It's a (program, number) tuple.
The number alone is ambiguous. It's a local identifier in a namespace defined by the program. Different programs define different namespaces. The same number in different namespaces means different things. Using the number without the namespace is like using a street address without the city — 123 Main Street exists in thousands of cities across America, and knowing the address without the city doesn't tell you where to go.
This seems obvious when stated explicitly. It is not obvious when you're staring at a failed transaction at 2 AM and the error code matches one you've seen before from a different program. The pattern-matching instinct kicks in: "I know this number. It means X. I know how to fix X." And you spend an hour fixing X before discovering that the number means Y this time, because it's coming from a different program.
The human brain is wired for pattern matching. See a number, recall what it meant last time, apply the same interpretation. This works when numbers have global meaning — when 404 always means "not found" regardless of which server returned it, when a red traffic light always means "stop" regardless of which intersection you're at. HTTP status codes are globally defined. Traffic signals are universally standardized.
Anchor error codes are not. They're locally defined, and the locality is the program. The number 6002 in one program's namespace is as unrelated to 6002 in another program's namespace as room 412 in a Manhattan hotel is unrelated to room 412 in a Chicago hotel. Same number. Different building. Different guest. Different everything.
What This Changes
Once I internalize the tuple model, my entire approach to error handling shifts. I stop building a single flat lookup table of "error code → meaning." Instead, I build per-program lookup tables. Error 6002 in Program A maps to meaning X. Error 6002 in Program B maps to meaning Y. The program ID is the primary key. The error code is the secondary key. Neither is sufficient alone.
My simulation workflow changes too. When a simulated transaction fails, I don't just extract the error code. I extract the error code and the program ID together. The simulation logs contain both. I parse them as a pair and look up the pair in the appropriate per-program table.
This also changes how I think about error handling in my own code. When my bot catches a transaction failure, it doesn't just log "error 6002." It logs "error 6002 from program [ID]." The program ID is part of the error identity. Logging the error without the program is like logging a phone call without the caller ID — you know something happened, but you don't know who's on the other end.
The insight extends beyond error codes. On-chain data in general is namespace-scoped. Account data layouts are program-specific. Instruction formats are program-specific. PDA derivation seeds are program-specific. The program ID is the universal disambiguator on Solana. It answers the question "whose rules apply here?" And that question must be answered before any interpretation of the data — error codes, account states, instruction parameters — can begin.
The Cost of Getting It Wrong
The half-day I spend debugging the wrong program's error enum is not the most expensive consequence. The most expensive consequence is the transactions I never debug at all.
When the bot encounters a failing cycle and the error code matches something I've "already fixed," the temptation is to apply the known fix automatically. Error 6002? Invalid pool state? Refresh the pool data and retry. Except this time, 6002 isn't invalid pool state. It's an authority mismatch from a different program. The automatic retry refreshes the pool data — which was already correct — and resubmits. Same error. Retry again. Same error. The bot burns through retries, wasting compute and time, applying the wrong fix to the wrong diagnosis of the wrong problem from the wrong program.
Automated error handling without program attribution is like a pharmacy's auto-refill system that matches medications by pill color instead of NDC number. "The patient got round white pills last time, so refill with round white pills." Except dozens of medications come in round white pills, and matching by appearance instead of by the unique drug identifier fills the wrong prescription.
The fix for automated handling is the same as the fix for manual debugging: include the program ID. The retry logic doesn't just check "is this error 6002?" It checks "is this error 6002 from Program A?" Different programs get different retry strategies. Same error code, different program, different response. The tuple is the key.
I adjust the bot's error classification to use (program_id, error_code) as the lookup key for retry decisions. Known transient errors from specific programs get automatic retries. Unknown combinations get logged for manual investigation. The false-match rate — applying the wrong fix because the error code matched but the program didn't — drops to zero.
An error code is not a number. It never is, anywhere in computing, but on Solana, where CPI chains create overlapping namespaces, the illusion that it's just a number is particularly dangerous. The first question is always the same: which program said this? Answer that, and the number starts talking. Skip it, and the number lies.
Disclaimer
This article is for informational and educational purposes only and does not constitute financial, investment, legal, or professional advice. Content is produced independently and supported by advertising revenue. While we strive for accuracy, this article may contain unintentional errors or outdated information. Readers should independently verify all facts and data before making decisions. Company names and trademarks are referenced for analysis purposes under fair use principles. Always consult qualified professionals before making financial or legal decisions.