Internal error: "TypeError: Cannot read property 'value' of undefined at line 342". Users see this and think the app is broken. Developers see it in logs and can debug. But showing technical errors to users is wrong—it's confusing, scary, and doesn't help them fix the problem. You need two error representations: user-friendly messages for the UI and detailed technical info for logs and debugging.
import { Data, Effect } from "effect"
// ============================================
// 1. Define errors with multiple representations
// ============================================
class ValidationError extends Data.TaggedError("ValidationError") {
constructor(
readonly field: string,
readonly value: any,
readonly reason: string
) {
super()
}
userMessage(): string {
const fieldLabel = this.field
.replace(/([A-Z])/g, " $1")
.toLowerCase()
.trim()
return `Invalid ${fieldLabel}: ${this.reason}`
}
techMessage(): string {
return `[ValidationError] Field '${this.field}' failed validation. Value: ${JSON.stringify(this.value)}. Reason: ${this.reason}`
}
loggingInfo(): Record<string, any> {
return {
type: "ValidationError",
field: this.field,
value: this.redactSensitive(this.value),
reason: this.reason,
timestamp: new Date().toISOString(),
}
}
private redactSensitive(value: any): any {
if (typeof value === "string") {
if (
this.field.toLowerCase().includes("password") ||
this.field.toLowerCase().includes("token") ||
this.field.toLowerCase().includes("secret")
) {
return "[REDACTED]"
}
if (this.field.toLowerCase().includes("email")) {
return value.replace(/(.{2})(.*)(@.*)/, "$1***$3")
}
}
return value
}
}
class NetworkError extends Data.TaggedError("NetworkError") {
constructor(
readonly statusCode: number,
readonly endpoint: string,
readonly originalError: Error
) {
super()
}
userMessage(): string {
switch (this.statusCode) {
case 500:
case 502:
case 503:
return "The service is temporarily unavailable. Please try again in a few moments."
case 404:
return "The resource you requested could not be found."
case 401:
return "Your session has expired. Please log in again."
case 403:
return "You don't have permission to access this resource."
case 429:
return "Too many requests. Please wait a moment before trying again."
default:
return "A network error occurred. Please check your connection and try again."
}
}
techMessage(): string {
return `[NetworkError] HTTP ${this.statusCode} from ${this.endpoint}. Cause: ${this.originalError.message}`
}
loggingInfo(): Record<string, any> {
return {
type: "NetworkError",
statusCode: this.statusCode,
endpoint: this.endpoint,
errorMessage: this.originalError.message,
stack: this.originalError.stack,
timestamp: new Date().toISOString(),
}
}
}
class PaymentError extends Data.TaggedError("PaymentError") {
constructor(
readonly reason: string,
readonly transactionId: string,
readonly attemptCount: number
) {
super()
}
userMessage(): string {
if (this.reason.includes("insufficient")) {
return "Your account has insufficient funds. Please add funds and try again."
}
if (this.reason.includes("expired")) {
return "Your payment method has expired. Please update it."
}
if (this.reason.includes("declined")) {
return "Your payment was declined. Please check your details and try again."
}
return "Payment processing failed. Please try again or contact support."
}
techMessage(): string {
return `[PaymentError] Transaction ${this.transactionId} failed after ${this.attemptCount} attempts. Reason: ${this.reason}`
}
loggingInfo(): Record<string, any> {
return {
type: "PaymentError",
transactionId: this.transactionId,
reason: this.reason,
attempts: this.attemptCount,
timestamp: new Date().toISOString(),
}
}
}
// ============================================
// 2. Error context for HTTP responses
// ============================================
type ErrorResponse = {
message: string
code: string
details?: Record<string, any>
}
const toErrorResponse = (
error: ValidationError | NetworkError | PaymentError
): ErrorResponse => {
return {
message: error.userMessage(),
code: error._tag,
details: error._tag === "ValidationError" ? { field: error.field } : undefined,
}
}
// ============================================
// 3. Logger service
// ============================================
class Logger {
static error(
error: ValidationError | NetworkError | PaymentError,
context?: Record<string, any>
): void {
const logData = {
...error.loggingInfo(),
...(context && { context }),
}
console.error("[ERROR LOG]", JSON.stringify(logData, null, 2))
// In production, this would send to external logging service
// e.g., Sentry, DataDog, CloudWatch
}
static techDebug(
error: ValidationError | NetworkError | PaymentError
): void {
console.debug("[TECH DEBUG]", error.techMessage())
}
}
// ============================================
// 4. Simulate domain operations
// ============================================
const validateEmail = (email: string): Effect.Effect<string, ValidationError> =>
Effect.gen(function* () {
if (!email.includes("@")) {
return yield* Effect.fail(
new ValidationError("email", email, "Must contain @ symbol")
)
}
return email
})
const processPayment = (
amount: number
): Effect.Effect<{ success: boolean }, PaymentError> =>
Effect.gen(function* () {
if (amount > 10000) {
return yield* Effect.fail(
new PaymentError(
"insufficient_balance",
"txn_12345",
1
)
)
}
return { success: true }
})
const fetchUserData = (): Effect.Effect<any, NetworkError> =>
Effect.gen(function* () {
return yield* Effect.fail(
new NetworkError(503, "https://api.example.com/users", new Error("Service unavailable"))
)
})
// ============================================
// 5. Application logic
// ============================================
const appLogic = Effect.gen(function* () {
console.log("=== Scenario 1: Validation Error ===\n")
const validationResult = yield* validateEmail("invalid-email").pipe(
Effect.either
)
if (validationResult._tag === "Left") {
const error = validationResult.left
console.log("📧 User sees:", error.userMessage())
console.log("🔧 Dev sees:", error.techMessage())
Logger.error(error, { context: "email_validation" })
console.log()
}
console.log("=== Scenario 2: Network Error ===\n")
const networkResult = yield* fetchUserData().pipe(
Effect.either
)
if (networkResult._tag === "Left") {
const error = networkResult.left
console.log("👤 User sees:", error.userMessage())
console.log("🔧 Dev sees:", error.techMessage())
Logger.error(error, { context: "user_data_fetch" })
console.log()
}
console.log("=== Scenario 3: Payment Error ===\n")
const paymentResult = yield* processPayment(50000).pipe(
Effect.either
)
if (paymentResult._tag === "Left") {
const error = paymentResult.left
console.log("💳 User sees:", error.userMessage())
console.log("🔧 Dev sees:", error.techMessage())
Logger.error(error, { context: "payment_processing", userId: "user_123" })
console.log()
}
console.log("=== Scenario 4: API Response ===\n")
const errors: (ValidationError | NetworkError | PaymentError)[] = []
const emailErr = yield* validateEmail("bad").pipe(Effect.either)
if (emailErr._tag === "Left") errors.push(emailErr.left)
const paymentErr = yield* processPayment(15000).pipe(Effect.either)
if (paymentErr._tag === "Left") errors.push(paymentErr.left)
console.log("📤 API Response to client:")
console.log(
JSON.stringify(
{
success: false,
errors: errors.map(toErrorResponse),
},
null,
2
)
)
console.log("\n📋 Log entries for monitoring:")
for (const error of errors) {
console.log(JSON.stringify(error.loggingInfo()))
}
return errors
})
// Run application
Effect.runPromise(appLogic)
.then(() => console.log("\n✅ Error messaging complete"))
.catch((error) => console.error(`Error: ${error.message}`))