xerrors.Err currently carries a Code (e.g. ErrNotFound, ErrInvalidInput) that maps to transport-layer status codes — HTTP 404, gRPC NOT_FOUND, etc. This is intentional and should not change.
However, as consumer applications grow multi-language frontends, there is a need for a second, independent error identity that operates at the platform/domain layer rather than the transport layer.
The problem
An HTTP 404 can mean many different things depending on context:
Employee not found
Role not found
Branch not found
A transport code (NOT_FOUND → 404) cannot distinguish between these. A frontend consuming the API cannot render a specific, actionable translated message — it can only show a generic fallback.
The same applies to 400s:
Email already registered
Role name already taken
Invalid UUID format
All map to ErrInvalidInput / ErrAlreadyExists at the transport layer, but each needs a distinct user-facing message in the consuming application.
Why not reuse Code
Code is intentionally transport-agnostic in the sense that it drives status code mapping:
REST: ErrNotFound → 404
gRPC: ErrNotFound → codes.NotFound
It must remain stable and bounded. Adding domain-specific values to it would pollute the transport mapping and break the clean separation between infrastructure concerns and business concerns.
Proposed solution
Add an optional PlatformCode string field to Err with a WithPlatformCode(code string) *Err builder method.
// Example usage in a servicereturnxerrors.New(xerrors.ErrNotFound,"employee not found").WithPlatformCode("EMPLOYEE_NOT_FOUND")returnxerrors.New(xerrors.ErrAlreadyExists,"email already registered").WithPlatformCode("EMAIL_ALREADY_EXISTS")
// Accessing in transport layervarxe*xerrors.Erriferrors.As(err,&xe){platformCode:=xe.PlatformCode()// "EMPLOYEE_NOT_FOUND"transportCode:=xe.Code()// xerrors.ErrNotFound}
Behaviour
PlatformCode is optional — not every error needs one. Internal errors, panics, and infrastructure failures do not benefit from platform codes (a 500 is always a generic "try again" regardless of cause).
PlatformCode is transport-agnostic — HTTP serializes it as "platformCode" in the JSON body; gRPC passes it in the error detail. The field travels unchanged across transports.
PlatformCode does not affect Code resolution or status code mapping in any way.
Expected JSON response shape (HTTP consumer)
{"code":"NOT_FOUND","platformCode":"EMPLOYEE_NOT_FOUND","message":"employee not found"}
The frontend team maintains their own dictionary mapping platformCode → translated user message. The backend stays in English throughout.
Acceptance criteria
Err has a platformCode string private field
WithPlatformCode(code string) *Err builder returns the same *Err for chaining
PlatformCode() string getter returns the value (empty string if not set)
Unit tests covering: set + get, chaining with WithContext, empty default
## Context
`xerrors.Err` currently carries a `Code` (e.g. `ErrNotFound`, `ErrInvalidInput`) that maps to transport-layer status codes — HTTP 404, gRPC `NOT_FOUND`, etc. This is intentional and should not change.
However, as consumer applications grow multi-language frontends, there is a need for a second, independent error identity that operates at the **platform/domain layer** rather than the transport layer.
### The problem
An HTTP 404 can mean many different things depending on context:
- Employee not found
- Role not found
- Branch not found
A transport code (`NOT_FOUND` → 404) cannot distinguish between these. A frontend consuming the API cannot render a specific, actionable translated message — it can only show a generic fallback.
The same applies to 400s:
- Email already registered
- Role name already taken
- Invalid UUID format
All map to `ErrInvalidInput` / `ErrAlreadyExists` at the transport layer, but each needs a distinct user-facing message in the consuming application.
### Why not reuse `Code`
`Code` is intentionally transport-agnostic in the sense that it drives status code mapping:
- REST: `ErrNotFound` → 404
- gRPC: `ErrNotFound` → `codes.NotFound`
It must remain stable and bounded. Adding domain-specific values to it would pollute the transport mapping and break the clean separation between infrastructure concerns and business concerns.
### Proposed solution
Add an optional `PlatformCode string` field to `Err` with a `WithPlatformCode(code string) *Err` builder method.
```go
// Example usage in a service
return xerrors.New(xerrors.ErrNotFound, "employee not found").
WithPlatformCode("EMPLOYEE_NOT_FOUND")
return xerrors.New(xerrors.ErrAlreadyExists, "email already registered").
WithPlatformCode("EMAIL_ALREADY_EXISTS")
```
```go
// Accessing in transport layer
var xe *xerrors.Err
if errors.As(err, &xe) {
platformCode := xe.PlatformCode() // "EMPLOYEE_NOT_FOUND"
transportCode := xe.Code() // xerrors.ErrNotFound
}
```
### Behaviour
- `PlatformCode` is **optional** — not every error needs one. Internal errors, panics, and infrastructure failures do not benefit from platform codes (a 500 is always a generic "try again" regardless of cause).
- `PlatformCode` is **transport-agnostic** — HTTP serializes it as `"platformCode"` in the JSON body; gRPC passes it in the error detail. The field travels unchanged across transports.
- `PlatformCode` does **not** affect `Code` resolution or status code mapping in any way.
### Expected JSON response shape (HTTP consumer)
```json
{
"code": "NOT_FOUND",
"platformCode": "EMPLOYEE_NOT_FOUND",
"message": "employee not found"
}
```
The frontend team maintains their own dictionary mapping `platformCode` → translated user message. The backend stays in English throughout.
## Acceptance criteria
- [ ] `Err` has a `platformCode string` private field
- [ ] `WithPlatformCode(code string) *Err` builder returns the same `*Err` for chaining
- [ ] `PlatformCode() string` getter returns the value (empty string if not set)
- [ ] Existing `Code`, `Error`, `WithContext` behaviour unchanged
- [ ] Unit tests covering: set + get, chaining with `WithContext`, empty default
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Context
xerrors.Errcurrently carries aCode(e.g.ErrNotFound,ErrInvalidInput) that maps to transport-layer status codes — HTTP 404, gRPCNOT_FOUND, etc. This is intentional and should not change.However, as consumer applications grow multi-language frontends, there is a need for a second, independent error identity that operates at the platform/domain layer rather than the transport layer.
The problem
An HTTP 404 can mean many different things depending on context:
A transport code (
NOT_FOUND→ 404) cannot distinguish between these. A frontend consuming the API cannot render a specific, actionable translated message — it can only show a generic fallback.The same applies to 400s:
All map to
ErrInvalidInput/ErrAlreadyExistsat the transport layer, but each needs a distinct user-facing message in the consuming application.Why not reuse
CodeCodeis intentionally transport-agnostic in the sense that it drives status code mapping:ErrNotFound→ 404ErrNotFound→codes.NotFoundIt must remain stable and bounded. Adding domain-specific values to it would pollute the transport mapping and break the clean separation between infrastructure concerns and business concerns.
Proposed solution
Add an optional
PlatformCode stringfield toErrwith aWithPlatformCode(code string) *Errbuilder method.Behaviour
PlatformCodeis optional — not every error needs one. Internal errors, panics, and infrastructure failures do not benefit from platform codes (a 500 is always a generic "try again" regardless of cause).PlatformCodeis transport-agnostic — HTTP serializes it as"platformCode"in the JSON body; gRPC passes it in the error detail. The field travels unchanged across transports.PlatformCodedoes not affectCoderesolution or status code mapping in any way.Expected JSON response shape (HTTP consumer)
The frontend team maintains their own dictionary mapping
platformCode→ translated user message. The backend stays in English throughout.Acceptance criteria
Errhas aplatformCode stringprivate fieldWithPlatformCode(code string) *Errbuilder returns the same*Errfor chainingPlatformCode() stringgetter returns the value (empty string if not set)Code,Error,WithContextbehaviour unchangedWithContext, empty default