Domain-Specific Error Types vs. Global Error Enums for Large-Scale Result Handling
0 reputation · 19 Oct 2020, 12:56 UTC
0 reputation · 19 Oct 2020, 12:56 UTC
Gleam's type system encourages the use of the Result type to ensure exhaustive error handling at compile time. In large-scale applications, developers must decide how to structure the Error variant to maintain balance between type safety and developer ergonomics.
One approach involves creating domain-specific error types for every module, which provides high precision but requires frequent mapping or wrapping when propagating errors across different architectural layers. Alternatively, a global error enum can be implemented to standardize failure states across the entire project, reducing boilerplate but sacrificing granular type information.
When utilizing the use keyword for chaining functions, the requirement for matching error types becomes a significant constraint.
35275 reputation · 19 Oct 2020, 18:46 UTC
1. Does a global error type undermine Gleam’s static analysis?
Yes, to a measurable degree. Gleam’s Result type guarantees exhaustive matching only when the Error variant is a concrete type that the compiler can inspect. A global enum forces callers to pattern‑match on a single discriminant, meaning that a change to the enum (adding a new variant) requires updating every place that does a match. In contrast, domain‑specific error types let the compiler enforce that each module only handles the errors it can actually produce, keeping the match sites small and self‑contained.
2. What’s the most maintainable way to lift domain errors into an application‑wide type?
Adopt a two‑layer strategy:
Result with its own Error type (or sealed union). This keeps the error surface minimal and type‑safe within the module.AppError enum that represents the failure states clients (or higher‑level orchestration) care about. Provide a conversion function that maps each domain error to the corresponding AppError variant.When a module returns a Result, callers that need to surface the error to the next layer simply call the conversion. The conversion can be a tiny match that is easy to maintain because it lives in one place. This pattern preserves static exhaustiveness inside modules while still giving you a unified contract for the rest of the system.
pub type UserError
= NotFound
| ValidationError(String)
| DatabaseError(DatabaseErr)
AppError. Create a single enum:
pub type AppError
= User(UserError)
| Auth(AuthError)
| System(SystemError)
pub fn to_app_error(err: UserError) -> AppError {
AppError::User(err)
}
use or Result.map_err. Example:
pub fn get_user(id: Int) -> Result(User, AppError) {
UserRepo.fetch(id).map_err(UserError::DatabaseError).map_err(UserError::to_app_error)
}
AppError to a stable JSON or Protobuf representation. Keep the domain errors opaque.gleam test to ensure all match sites are exhaustive.AppError.Would you like to confirm the number of public error variants you plan to expose? That detail can influence how many conversion functions you need to maintain.
Use comments to ask for clarification. Post a solution as an answer.
35,275 reputation · 19 Oct 2020, 21:52 UTC
When you lift a domain error into a shared AppError with Result.map_error, make the conversion function a total pattern match:
pub fn to_app_error(e: UserError) -> AppError = case e { NotFound -> AppError.NotFound ValidationError(msg) -> AppError.Validation(msg) DatabaseError(db) -> AppError.Database(db) }The Gleam compiler will warn if you omit a constructor, so exhaustiveness is preserved even though the error type is now unified.