Dev hay dùng try/catch một cách "bản năng": bao hết, catch hết, swallow hết. Hoặc ngược lại: throw exception cho MỌI validation error. Cả hai đều sai. Session này giúp bạn thiết kế error handling có chiến lược.
Mục tiêu
- Phân biệt expected errors vs unexpected exceptions
- Biết khi nào throw, khi nào return Result
- Thiết kế global error handling đúng cách
- Tránh các anti-patterns phổ biến
1) Hai Loại Lỗi - Phải Phân Biệt
Expected Errors (Business Errors):
- User nhập sai email
- Sản phẩm hết hàng
- Đơn hàng đã bị hủy
→ KHÔNG phải exception! Đây là flow BÌNH THƯỜNG.
→ Trả Result/Error object.
Unexpected Exceptions (System Errors):
- Database connection timeout
- Null reference (bug)
- Out of memory
→ ĐÚNG LÀ exception! Không lường trước được.
→ Throw exception, catch ở global handler.
Tại sao phân biệt?
csharp// ❌ Dùng Exception cho business errors
public Order CreateOrder(OrderRequest req)
{
if (req.Items.Count == 0)
throw new ValidationException("Order must have items");
// Exception = tốn kém (stack trace, unwinding)
// Exception = control flow (anti-pattern!)
// Exception = caller PHẢI try/catch → messy code
}
// ✅ Dùng Result cho business errors
public Result<Order> CreateOrder(OrderRequest req)
{
if (req.Items.Count == 0)
return Result.Failure<Order>("Order must have items");
// Rẻ (no stack trace)
// Explicit (caller BIẾT phải check result)
// Clean flow (no try/catch)
}
2) Result Pattern - Cách Xử Lý Business Errors
csharp// Minimal Result pattern
public class Result<T>
{
public T? Value { get; }
public string? Error { get; }
public bool IsSuccess => Error is null;
private Result(T? value, string? error)
{
Value = value;
Error = error;
}
public static Result<T> Success(T value) => new(value, null);
public static Result<T> Failure(string error) => new(default, error);
}
// Usage trong handler
public async Task<Result<OrderDto>> CreateOrder(CreateOrderCommand cmd)
{
var product = await _productRepo.GetByIdAsync(cmd.ProductId);
if (product is null)
return Result<OrderDto>.Failure("Product not found");
if (product.Stock < cmd.Quantity)
return Result<OrderDto>.Failure("Insufficient stock");
var order = Order.Create(cmd.CustomerId, product, cmd.Quantity);
await _orderRepo.AddAsync(order);
return Result<OrderDto>.Success(order.ToDto());
}
// Controller
[HttpPost]
public async Task<IActionResult> Create(CreateOrderCommand cmd)
{
var result = await _handler.CreateOrder(cmd);
return result.IsSuccess
? Ok(result.Value)
: BadRequest(new { error = result.Error });
// Clean! Không try/catch.
}
3) Exception - Chỉ Cho Unexpected Errors
csharp// Rule: Exception = điều KHÔNG LƯỜNG TRƯỚC xảy ra
// ❌ Catch rồi nuốt
try
{
await db.SaveChangesAsync();
}
catch (Exception)
{
// Nuốt exception → bug ẩn, data corruption
// KHÔNG BAO GIỜ catch exception mà không xử lý!
}
// ❌ Catch quá rộng rồi làm gì đó sai
try
{
var user = await GetUserAsync(id);
}
catch (Exception ex)
{
return null; // Trả null → NullReferenceException ở chỗ khác → khó debug!
}
// ✅ Let it bubble up → Global handler bắt
// KHÔNG catch ở đây trừ khi bạn CÓ THỂ xử lý meaningfully
var user = await GetUserAsync(id); // Nếu fail → exception bubble up
Khi nào NÊN try/catch?
csharp// 1. External call CÓ THỂ fail và bạn có FALLBACK
try
{
var rate = await exchangeRateApi.GetRate("USD", "VND");
return rate;
}
catch (HttpRequestException)
{
// Fallback: dùng cached rate
return _cache.GetLastKnownRate("USD", "VND");
}
// 2. Resource cleanup (should use `using` thay thế)
// 3. Retry logic (should use Polly thay thế)
// Rule: TRY/CATCH = bạn CÓ HÀNH ĐỘNG cụ thể cho exception đó
// Nếu chỉ log → để global handler lo
4) Global Exception Handling - 1 Nơi Duy Nhất
csharp// ASP.NET Core: IExceptionHandler (.NET 8+)
public class GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
: IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken ct)
{
var (statusCode, message) = exception switch
{
ValidationException ex => (400, ex.Message),
NotFoundException ex => (404, ex.Message),
UnauthorizedAccessException => (403, "Forbidden"),
_ => (500, "Internal server error")
};
logger.LogError(exception, "Unhandled exception: {Message}", exception.Message);
httpContext.Response.StatusCode = statusCode;
await httpContext.Response.WriteAsJsonAsync(new ProblemDetails
{
Status = statusCode,
Title = message,
// ⚠️ KHÔNG expose stack trace trong production!
Detail = httpContext.RequestServices
.GetRequiredService<IHostEnvironment>().IsDevelopment()
? exception.StackTrace
: null
}, ct);
return true; // Exception handled
}
}
// Registration
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
app.UseExceptionHandler();
5) Custom Exception Hierarchy - Giữ Gọn
csharp// ❌ Over-engineering: 50 exception types
public class UserNotFoundException : Exception { }
public class UserAlreadyExistsException : Exception { }
public class UserEmailInvalidException : Exception { }
// ... quá nhiều!
// ✅ Giữ gọn: 2-3 base exception types + Result pattern
// Layer 1: Business errors → Result pattern (không throw)
// Layer 2: Application exceptions → 2-3 types
public class NotFoundException(string entity, object key)
: Exception($"{entity} with key '{key}' not found");
public class ConflictException(string message)
: Exception(message);
public class ForbiddenException(string message)
: Exception(message);
// Tất cả những thứ khác → default 500 bởi global handler
6) Validation - Ở Đâu, Bằng Gì?
Request validation (API boundary):
→ FluentValidation / Data Annotations
→ Validate TRƯỚC khi vào business logic
→ Return 400 Bad Request
Business validation (Domain):
→ Trong Entity/Domain Service
→ Return Result.Failure (không throw)
Infrastructure validation:
→ Try/catch cho external failures (DB, API)
→ Global handler bắt
csharp// Layer 1: Request validation (middleware/filter)
public class CreateOrderValidator : AbstractValidator<CreateOrderCommand>
{
public CreateOrderValidator()
{
RuleFor(x => x.ProductId).NotEmpty();
RuleFor(x => x.Quantity).GreaterThan(0);
}
}
// → Fail = 400 Bad Request (trước khi code chạy)
// Layer 2: Business validation (domain)
public Result<Order> CreateOrder(Guid productId, int quantity)
{
var product = await _repo.FindAsync(productId);
if (product is null)
return Result.Failure<Order>("Product not found");
if (product.Stock < quantity)
return Result.Failure<Order>("Insufficient stock");
// ...
}
// → Fail = 400/422 (business rule violated)
// Layer 3: Infrastructure (global handler)
// DbException, HttpRequestException → 500/503
Master Checklist ✅
□ Business errors → Result pattern (không throw exception)?
□ Unexpected errors → let bubble up, global handler bắt?
□ Không catch rồi nuốt (empty catch)?
□ Không catch Exception quá rộng (trừ global handler)?
□ Global handler log error VÀ trả ProblemDetails?
□ Production KHÔNG expose stack trace?
□ Validation ở đúng layer (request → business → infrastructure)?
□ Custom exceptions ≤5 types?
Tư duy master: "Nếu tôi catch exception này, tôi sẽ làm gì CỤ THỂ?" - Nếu trả lời được (retry, fallback, custom response) → catch. Nếu chỉ "log rồi throw lại" → để global handler lo.