Toddler is a Go package for structured error handling with custom 4-digit status codes, extending standard HTTP status semantics. It helps categorize and manage errors more granularly, with clear separation of public and internal values.
After rewriting custom error handling in nearly every Go project I've touched, I finally decided to create a reusable library. There's no magic here — just structured error handling with custom status codes, and clear separation of public and private values.
The Error struct represents a custom error with a status code, public message, service message, and metadata. Here’s an example of creating and using the Error struct:
package main
import (
"fmt"
"github.com/beka-birhanu/toddler/error"
"github.com/beka-birhanu/toddler/status"
)
func main() {
err := &error.Error{
StatusCode: status.UnauthorizedInvalidCredential,
PublicMessage: "Incorrect username or password. Please try again.",
ServiceMessage: "Incorrect password attempt for user ID: 6eb3d746-1be1-445e-9d76-5aa996754dbd",
PublicMetaData: map[string]string{
"error_type": "authentication",
"hint": "Please check your credentials.",
},
ServiceMetaData: map[string]string{
"timestamp": "2025-05-06T10:00:00Z",
"user_id": "6eb3d746-1be1-445e-9d76-5aa996754dbd",
},
}
// Print error with detailed information
fmt.Println(err.Error())
/* Output:
{
status: Unauthorized_InvalidCredential (4011),
publicMessage: 'Incorrect username or password. Please try again.',
serviceMessage: 'Incorrect password attempt for user ID: 6eb3d746-1be1-445e-9d76-5aa996754dbd',
publicMetaData: {error_type: 'authentication', hint: 'Please check your credentials.'},
serviceMetaData: {timestamp: '2025-05-06T10:00:00Z', user_id: '6eb3d746-1be1-445e-9d76-5aa996754dbd'}
}
*/
}When an error occurs, sensitive internal information (e.g., database details) should not be exposed in public-facing messages. Neutralizing maps detailed error codes to more general ones, preventing the leak of internal specifics.
err := &error.Error{
StatusCode: status.ServerErrorDatabase,
PublicMessage: "Internal server error.",
ServiceMessage: "Database connection failed.",
}
// Print error before neutralization
fmt.Println("Before Neutralization: ", err.Error())
// Neutralize error code
err.NeutralizeOverDetailedStatus()
// Print error after neutralization
fmt.Println("After Neutralization: ", err.Error())Before Neutralization:
{
status: ServerError_Database (5001),
publicMessage: 'Internal server error.',
serviceMessage: 'Database connection failed.',
}
After Neutralization:
{
status: ServerError (5000),
publicMessage: 'Internal server error.',
serviceMessage: 'Database connection failed.',
}
The status codes in this package are organized into groups based on HTTP semantics:
- 4000-4009 (Bad Request): Errors related to invalid user input or malformed requests.
- 4010-4019 (Unauthorized): Authentication or authorization failures.
- 4030-4039 (Forbidden): Access control issues.
- 4040-4049 (Not Found): Resources not found.
- 5000-5009 (Server Error): Internal server errors or service failures.
Each error code has a corresponding human-readable name, which can be retrieved using status.GetErrorName(code).
fmt.Println(status.GetErrorName(status.BadRequestMissingField))
// Output: "BadRequest_MissingField"All status codes extend standard HTTP semantics plus one more digit (4-digit codes) to improve clarity in error handling.
| HTTP Status (First 3 Digits) | Custom Status Code Range (Custom Status Code) |
|---|---|
| 400 Bad Request | 4000 - 4009 |
| - 4000: BadRequest | |
| - 4001: BadRequestMissingField | |
| - 4002: BadRequestTypeMismatch | |
| - 4003: BadRequestFieldConstraint | |
| - 4004: BadRequestInvalidFormat | |
| - 4005: BadRequestOutOfRange | |
| - 4006: BadRequestInvalidValue | |
| - 4007: BadRequestEnumViolation | |
| 401 Unauthorized | 4010 - 4019 |
| - 4010: Unauthorized | |
| - 4011: UnauthorizedInvalidCredential | |
| - 4012: UnauthorizedTokenRequired | |
| - 4013: UnauthorizedInvalidToken | |
| 403 Forbidden | 4030 - 4039 |
| - 4030: Forbidden | |
| - 4031: ForbiddenNotEnoughPrivilege | |
| - 4032: ForbiddenOnlyOwners | |
| 404 Not Found | 4040 - 4049 |
| - 4040: NotFound | |
| - 4041: NotFoundResource | |
| 409 Conflict | 4090 - 4099 |
| - 4090: Conflict | |
| - 4090: ConflictDuplicateData | |
| 500 Server Error | 5000 - 5009 |
| - 5000: ServerError | |
| - 5001: ServerErrorDatabase | |
| - 5002: ServerErrorServiceCommunication |
It includes error mapper for postgresql and validator erros.
Purpose: Maps low-level PostgreSQL errors into structured, application-specific error types that include detailed status codes, public-safe messages, and internal metadata for debugging.
| Database Error Type | Mapped Application Error |
|---|---|
sql.ErrNoRows |
status.NotFoundResource |
PostgreSQL Unique Constraint (23505) |
status.ConflictDuplicateData |
Foreign Key Violation (23503) |
status.BadRequest (invalid reference) |
Not Null Violation (23502) |
status.BadRequest (missing field) |
Check Constraint (23514) |
status.BadRequest (failed validation) |
| Unhandled PostgreSQL Error | status.ServerErrorDatabase |
| Unknown Errors | status.ServerErrorDatabase |
func FromValidationErrors(err error) *error.ErrorFromValidationErrors converts go-playground/validator validation errors into a structured application-level error (*error.Error). It makes it easy to return clear and helpful feedback to users while preserving rich diagnostic details for logging and debugging.
type SignupInput struct {
Email string `validate:"required,email"`
Age int `validate:"required,gte=18"`
}
func ValidateSignupInput(input SignupInput) *error.Error {
err := validator.New().Struct(input)
return error.FromValidationErrors(err)
}| Category | Tags | Status Code |
|---|---|---|
| Required | required, required_with, ... |
status.BadRequestMissingField |
| Format / Pattern | email, uuid, json, ... |
status.BadRequestInvalidFormat |
| Range / Length | min, max, len, gt, lte, ... |
status.BadRequestOutOfRange |
| Enum / One of | oneof |
status.BadRequestEnumViolation |
| Value Constraints | eq, ne, unique, ... |
status.BadRequestInvalidValue |
| Unknown | Anything not explicitly mapped | status.BadRequest |
{
"PublicStatusCode": 4001,
"PublicMessage": "Invalid input in one or more fields",
"PublicMetaData": {
"error_type": "Validation",
"fields": "Email, Age",
"failures": "Email: Email must be a valid email; Age: Age must be gte 18"
},
"ServiceMetaData": {
"error_type": "ValidatorFieldErrors",
"fields": "Email, Age",
"details": {
"Emailreason": "email",
"Emailstatus_code": "4003",
"Agereason": "gte",
"Agestatus_code": "4004"
}
}
}