C# Day 9: Exception Handling
Ngày 9 tập trung vào Exception Handling.
Mục tiêu không chỉ là biết viết:
try
{
}
catch
{
}
mà phải hiểu:
- exception là gì?
- exception thay đổi control flow thế nào?
- exception propagation là gì?
throwkhácthrow exra sao?- khi nào nên catch?
- khi nào không nên catch?
- custom exception dùng khi nào?
- validation có nên throw exception không?
- exception khác Result Pattern thế nào?
- trong ASP.NET Core nên xử lý exception ở đâu?
- log exception thế nào để giữ stack trace?
finally,using,IDisposableliên quan gì tới exception?
1. Exception là gì?
Exception là một object đại diện cho một lỗi bất thường xảy ra trong lúc chương trình đang chạy.
Ví dụ:
var number = int.Parse("abc");
có thể throw:
FormatException
Hoặc:
var item = list[10];
khi list không có index đó có thể throw:
ArgumentOutOfRangeException
Mental model:
Normal flow
↓
something goes wrong
↓
Exception object được throw
↓
normal flow bị dừng
↓
runtime tìm catch phù hợp
2. Exception là một Object
Ví dụ:
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
ex là một object.
Nó có các thông tin như:
Message
StackTrace
InnerException
Source
Data
Ví dụ:
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
Mental model:
Exception
→ lỗi + context về lỗi
3. try
try chứa code có khả năng throw exception.
try
{
DoWork();
}
Nếu không có lỗi:
try
↓
DoWork()
↓
continue normal flow
Nếu có lỗi:
try
↓
DoWork()
↓
throw exception
↓
normal flow dừng
↓
runtime tìm catch
4. catch
Ví dụ:
try
{
DoWork();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
catch xử lý exception phù hợp.
Mental model:
try
→ run risky code
catch
→ handle failure
5. finally
Ví dụ:
try
{
DoWork();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
Console.WriteLine("Cleanup");
}
finally dùng cho cleanup cần chạy dù success hay exception.
Mental model:
success
→ finally
exception caught
→ finally
exception rethrown
→ finally trước khi propagate tiếp
6. Flow đầy đủ của try/catch/finally
try
{
Console.WriteLine("A");
DoWork();
Console.WriteLine("B");
}
catch (Exception ex)
{
Console.WriteLine("C");
}
finally
{
Console.WriteLine("D");
}
Nếu DoWork() thành công:
A
B
D
Nếu DoWork() throw:
A
C
D
B không chạy vì control flow đã rời khỏi try.
7. throw
Ví dụ:
if (user is null)
{
throw new InvalidOperationException(
"User was not found."
);
}
Mental model:
throw
→ dừng normal flow
→ tạo/chuyển exception lên call stack
8. Khi nào nên throw?
Ví dụ:
public void Withdraw(decimal amount)
{
if (amount <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(amount)
);
}
}
Ở đây argument invalid có thể được xem là programming/contract error.
Nhưng không phải mọi business outcome đều nên throw.
Ví dụ:
Coupon hết hạn
User không có quyền redeem
Stock đã hết
có thể phù hợp hơn với:
Result
Validation result
Domain result
tùy architecture.
9. throw; vs throw ex;
Đây là câu interview rất quan trọng.
Không nên:
catch (Exception ex)
{
throw ex;
}
Vì:
throw ex
→ có thể reset/alter stack trace origin
→ làm mất context nơi exception ban đầu xảy ra
Nên:
catch (Exception)
{
throw;
}
Mental model:
throw;
→ rethrow exception hiện tại
→ preserve original stack trace tốt hơn
10. Ví dụ throw đúng
try
{
repository.Save();
}
catch (Exception)
{
throw;
}
Nhưng đoạn trên còn một vấn đề:
catch rồi chỉ throw lại
→ thường không có giá trị
Nếu không:
log
translate
recover
add context
cleanup
thì thường không cần catch.
11. Exception Propagation
Giả sử:
Controller
↓
Service
↓
Repository
↓
Database
Repository throw exception:
throw new DatabaseException();
Nếu Repository không catch:
Repository
↑
Service
↑
Controller
↑
Global Exception Handler
Đây gọi là:
Exception Propagation
Mental model:
Exception
→ đi ngược call stack
→ cho tới khi gặp catch phù hợp
12. Call Stack
Ví dụ:
Controller()
{
Service();
}
Service()
{
Repository();
}
Repository()
{
throw new Exception();
}
Call stack:
Controller
↓
Service
↓
Repository
↓
throw
Sau khi throw:
Repository
↑
Service
↑
Controller
runtime tìm nơi có thể handle exception.
13. Multiple Catch Blocks
Ví dụ:
try
{
await DoWorkAsync();
}
catch (TimeoutException ex)
{
// timeout-specific handling
}
catch (UnauthorizedAccessException ex)
{
// permission-specific handling
}
catch (Exception ex)
{
// fallback
}
Rule:
specific exception
→ trước
general exception
→ sau
14. Tại sao Catch Specific Exception tốt hơn?
Bad:
catch (Exception)
{
return false;
}
Bạn đang coi mọi failure giống nhau.
Trong thực tế:
Timeout
≠
Validation Error
≠
Unauthorized
≠
Database Constraint Violation
≠
Bug
Specific catch giúp có handling phù hợp.
15. Exception Filter
Ví dụ:
catch (HttpRequestException ex)
when (
ex.StatusCode
== HttpStatusCode.NotFound
)
{
}
Mental model:
catch type
+
condition
16. Exception Filter hữu ích ở đâu?
Ví dụ:
catch (SqlException ex)
when (ex.Number == 2627)
{
// unique constraint violation
}
Bạn chỉ handle exception nếu condition match.
Ưu điểm:
không cần catch rồi if/throw lại quá nhiều
17. Custom Exception
Ví dụ:
public sealed class UserNotFoundException
: Exception
{
public Guid UserId { get; }
public UserNotFoundException(Guid userId)
: base(
$"User {userId} was not found."
)
{
UserId = userId;
}
}
Dùng:
throw new UserNotFoundException(userId);
18. Khi nào Custom Exception hữu ích?
Khi exception cần thể hiện meaning rõ ràng hơn.
Ví dụ:
UserNotFoundException
CouponExpiredException
DuplicateEmailException
PaymentFailedException
thay vì:
throw new Exception("Something wrong");
19. Không nên tạo Custom Exception vô tội vạ
Không cần tạo một custom exception cho mọi case.
Ví dụ:
NameEmptyException
AgeLessThan18Exception
EmailInvalidFormatException
có thể quá mức nếu đây chỉ là validation.
Rule:
Custom Exception
→ meaningful exceptional category
Validation
→ expected invalid input
20. InnerException
Ví dụ:
try
{
await repository.SaveAsync();
}
catch (DbException ex)
{
throw new OrderSaveException(
"Could not save the order.",
ex
);
}
ex trở thành:
InnerException
Mental model:
OrderSaveException
→ business/application context
InnerException
→ technical/root cause
21. Vì sao InnerException quan trọng?
Nếu chỉ:
throw new OrderSaveException(
"Save failed."
);
thì bạn có thể mất technical cause.
Nếu:
throw new OrderSaveException(
"Save failed.",
ex
);
thì chain:
OrderSaveException
↓
DbException
↓
provider/database detail
được giữ lại.
22. Exception Wrapping
Pattern:
Low-level exception
↓
catch
↓
wrap với exception có context cao hơn
↓
preserve InnerException
Ví dụ:
catch (SqlException ex)
{
throw new UserRepositoryException(
"Failed to load user.",
ex
);
}
Cần dùng có chủ đích, không wrap ở mọi layer nếu chỉ làm exception chain dài vô ích.
23. finally dùng cho Resource Cleanup
Ví dụ:
FileStream? stream = null;
try
{
stream = File.OpenRead(path);
// use stream
}
finally
{
stream?.Dispose();
}
Mục tiêu:
dù success hay fail
→ resource vẫn được cleanup
24. using giúp đơn giản hóa cleanup
Thay vì:
FileStream? stream = null;
try
{
stream = File.OpenRead(path);
}
finally
{
stream?.Dispose();
}
có thể:
using var stream =
File.OpenRead(path);
Compiler/runtime semantics đảm bảo Dispose() được gọi khi rời scope phù hợp.
25. IDisposable
Một object implement:
IDisposable
có cleanup contract:
void Dispose();
Ví dụ:
FileStream
DbConnection
StreamReader
Mental model:
GC
→ quản lý managed memory
Dispose
→ cleanup resource cần deterministic release
Không nên nhầm hai khái niệm.
26. Exception và using
Ví dụ:
using var stream =
File.OpenRead(path);
Process(stream);
Nếu Process() throw:
exception
↓
scope bị rời
↓
Dispose được gọi
↓
exception tiếp tục propagate
Đây là lý do using rất quan trọng.
27. Expected Error vs Exceptional Error
Đây là phần quan trọng về design.
Ví dụ expected outcome:
Email đã tồn tại
Coupon hết hạn
User nhập sai OTP
Balance không đủ
Những case này có thể là một phần bình thường của business flow.
Ví dụ exceptional:
Database unavailable
Corrupted data
Unexpected null invariant
Third-party service crashed unexpectedly
Mental model:
Expected business outcome
→ Result / Validation / explicit state
Unexpected exceptional failure
→ Exception
28. Validation vs Exception
Ví dụ request:
{
"email": "",
"age": 10
}
Validation fail là expected input problem.
Có thể trả:
400 Bad Request
+
validation errors
Không nhất thiết phải dùng exception cho từng field.
29. Result Pattern
Ví dụ:
public Result<User> FindUser(Guid id)
{
var user =
users.FirstOrDefault(
x => x.Id == id
);
if (user is null)
{
return Result<User>.NotFound();
}
return Result<User>.Success(user);
}
Mental model:
Result
→ explicit outcome
30. Exception vs Result Pattern
Exception:
implicit propagation
exceptional flow
stack trace
can bubble automatically
Result:
explicit return value
expected business outcome
caller phải handle
Rule đơn giản:
Expected outcome
→ Result thường phù hợp
Unexpected failure
→ Exception thường phù hợp
Không phải absolute rule; architecture quyết định.
31. Không dùng Exception như Normal Control Flow
Bad:
try
{
var user =
users.Single(
x => x.Id == id
);
}
catch
{
// user not found
}
Nếu not-found là bình thường:
var user =
users.FirstOrDefault(
x => x.Id == id
);
if (user is null)
{
// handle not found
}
32. Vì sao Exception Control Flow tệ?
Exception có cost cao hơn normal branch và làm intent khó đọc.
Bad mental model:
try something
↓
throw để quyết định logic bình thường
Better:
explicit condition
↓
explicit result
33. Không Swallow Exception
Bad:
try
{
DoWork();
}
catch
{
}
Lỗi bị mất hoàn toàn.
Hậu quả:
khó debug
không log
không retry
không signal failure
34. Catch thì phải có Purpose
Catch thường nên làm ít nhất một việc có ý nghĩa:
recover
translate
add context
log
cleanup
retry
map error
Nếu không làm gì:
đừng catch
35. Logging Exception đúng cách
Không nên chỉ:
_logger.LogError(ex.Message);
Bạn mất:
stack trace
exception type
inner exception
Nên:
_logger.LogError(
ex,
"Failed to process order {OrderId}",
orderId
);
36. Structured Logging
Ví dụ:
_logger.LogError(
ex,
"Failed to process order {OrderId} for user {UserId}",
orderId,
userId
);
Giúp logging system query theo:
OrderId
UserId
Exception type
TraceId
37. Không Log cùng một Exception ở mọi Layer
Ví dụ:
Repository log
↓
Service log
↓
Controller log
↓
Middleware log
cùng một exception có thể tạo 4 log records.
Thường nên có strategy rõ:
log ở boundary phù hợp
Ví dụ:
global exception handler
hoặc nơi thật sự recover/translate.
38. ASP.NET Core Global Exception Handling
Không nên:
try
{
...
}
catch
{
...
}
ở mọi controller action.
Mental model:
Controller
↓
Application Service
↓
Repository
↓
Exception
↑
Global Exception Handler
↓
HTTP Response
39. Mapping Exception sang HTTP Status
Ví dụ:
ValidationException
→ 400
Authentication failure
→ 401
Forbidden
→ 403
NotFoundException
→ 404
ConflictException
→ 409
Unexpected exception
→ 500
Mapping cụ thể phụ thuộc API semantics.
40. ProblemDetails
ASP.NET Core APIs có thể trả error theo Problem Details shape.
Ví dụ conceptual:
{
"type": "https://example.com/errors/not-found",
"title": "Resource not found",
"status": 404,
"detail": "User was not found.",
"traceId": "..."
}
Ưu điểm:
consistent API error contract
41. Global Handler Concept
Pseudo-code:
public async Task HandleAsync(
HttpContext context,
Exception exception)
{
switch (exception)
{
case NotFoundException:
context.Response.StatusCode = 404;
break;
case ConflictException:
context.Response.StatusCode = 409;
break;
default:
context.Response.StatusCode = 500;
break;
}
}
Mục tiêu:
centralized mapping
42. Không Expose Internal Exception ra Client
Bad:
{
"error": "Npgsql.PostgresException: relation users..."
}
Có thể leak:
DB structure
stack trace
internal path
implementation detail
Client chỉ cần safe error information.
Technical detail nên vào logs.
43. Retry có phải Catch Exception không?
Có thể.
Ví dụ:
Timeout
Temporary network failure
503 transient error
có thể retry.
Nhưng không nên retry mọi exception.
Ví dụ:
Validation Error
Unauthorized
Duplicate key
retry thường vô nghĩa.
Mental model:
Transient
→ maybe retry
Permanent/business failure
→ don't retry blindly
44. Catch Broad Exception ở Boundary
catch (Exception ex) không phải luôn xấu.
Nó phù hợp ở boundary như:
global exception handler
background worker loop
message consumer boundary
mục đích:
prevent process crash
log unexpected failure
map safe response
Nhưng business layer không nên catch Exception vô tội vạ.
45. Exception trong Background Worker
Ví dụ:
while (!stoppingToken.IsCancellationRequested)
{
try
{
await ProcessJobAsync();
}
catch (Exception ex)
{
logger.LogError(
ex,
"Job processing failed."
);
}
}
Nếu không catch ở worker boundary, một exception có thể làm loop/task chết.
Nhưng vẫn cần strategy:
retry
DLQ
backoff
alert
46. Exception trong Message Consumer
Mental model:
Message
↓
Process
↓
Exception
↓
Retry?
DLQ?
Nack?
Log?
Không chỉ catch rồi ignore.
Đây là phần nối sang Architecture sau này.
47. Exception không nên chứa Sensitive Data
Bad:
throw new Exception(
$"Login failed. Password={password}"
);
Không nên log:
password
token
secret
credit card
sensitive PII
Exception message có thể đi vào logging system.
48. Custom Exception Example hoàn chỉnh
public sealed class CouponExpiredException
: Exception
{
public Guid CouponId { get; }
public DateTimeOffset ExpiredAt { get; }
public CouponExpiredException(
Guid couponId,
DateTimeOffset expiredAt)
: base(
$"Coupon {couponId} expired at {expiredAt}."
)
{
CouponId = couponId;
ExpiredAt = expiredAt;
}
}
Usage:
if (coupon.EndTime < now)
{
throw new CouponExpiredException(
coupon.Id,
coupon.EndTime
);
}
Nhưng cần tự hỏi:
Coupon expired
→ exceptional?
hay
→ expected business result?
Không phải cứ có custom exception là design đúng.
49. Stack Trace
Stack trace cho biết call path nơi exception xảy ra.
Ví dụ conceptual:
UserRepository.Get()
at UserService.Get()
at UsersController.Get()
Đây là lý do cần preserve stack trace.
50. Vì sao throw ex nguy hiểm?
Ví dụ:
catch (Exception ex)
{
throw ex;
}
Có thể làm stack trace trông như exception bắt đầu lại tại line rethrow.
Trong khi:
catch
{
throw;
}
giữ origin rõ hơn.
Rule:
Rethrow same exception
→ throw;
51. Khi nào tạo Exception mới?
Nếu muốn translate abstraction.
Ví dụ:
catch (SqlException ex)
{
throw new UserRepositoryException(
"Failed to query users.",
ex
);
}
Ở đây:
SqlException
→ infrastructure-specific
UserRepositoryException
→ repository/application context
Nhưng nhớ preserve:
InnerException
52. Common Mistake — Catch quá rộng
Bad:
try
{
...
}
catch (Exception ex)
{
return null;
}
Bạn biến:
DB down
bug
timeout
invalid state
thành:
null
làm mất meaning của failure.
53. Common Mistake — Log rồi Throw sai
Bad:
catch (Exception ex)
{
logger.LogError(ex, "Failed");
throw ex;
}
Better:
catch (Exception ex)
{
logger.LogError(ex, "Failed");
throw;
}
Nhưng vẫn cần hỏi:
Có nên log ở layer này không?
54. Common Mistake — Return từ finally
Không nên:
finally
{
return;
}
Vì có thể làm control flow khó hiểu và che exception.
finally nên tập trung vào cleanup.
55. Common Mistake — Catch Exception rồi Ignore
Bad:
catch (Exception)
{
// ignore
}
Chỉ nên ignore exception cực kỳ có chủ đích và có reason rõ ràng.
56. Common Mistake — Exception cho Validation thường xuyên
Bad:
if (string.IsNullOrEmpty(request.Name))
{
throw new NameRequiredException();
}
Nếu đây là normal input validation, structured validation result thường rõ hơn.
57. Production Example — Service Layer
public async Task<UserDto> GetUserAsync(
Guid userId)
{
var user =
await repository.FindAsync(userId);
if (user is null)
{
throw new UserNotFoundException(
userId
);
}
return mapper.Map<UserDto>(user);
}
Nếu architecture chọn exception-based not-found, global handler có thể map:
UserNotFoundException
→ 404
58. Production Example — Result-Based Service
Alternative:
public async Task<Result<UserDto>>
GetUserAsync(Guid userId)
{
var user =
await repository.FindAsync(userId);
if (user is null)
{
return Result<UserDto>.NotFound();
}
return Result<UserDto>.Success(
mapper.Map<UserDto>(user)
);
}
Không có một đáp án tuyệt đối.
Trade-off nằm ở:
expected outcome?
exceptional failure?
team convention?
architecture?
59. Mental Model Tổng Kết
try
→ risky code
catch
→ handle known failure
finally
→ cleanup
throw
→ enter exception flow
throw;
→ rethrow same exception
InnerException
→ preserve original cause
Custom Exception
→ meaningful error category
Result
→ explicit expected outcome
Global Handler
→ centralized exception → HTTP mapping
60. Khi nào Catch?
Catch khi bạn có thể:
recover
translate
add context
retry
log at proper boundary
map response
cleanup
Nếu không:
let it propagate
61. Exception Decision Tree
Something went wrong
↓
Expected business outcome?
├── Yes
│ ↓
│ validation/result/explicit state
│
└── No
↓
Exception
↓
Can current layer handle meaningfully?
├── Yes
│ ↓
│ recover / translate / retry
│
└── No
↓
propagate
62. Interview Questions
1. Exception là gì?
Object đại diện cho failure bất thường trong runtime và thay đổi normal control flow.
2. throw khác throw ex thế nào?
throw; rethrow current exception và preserve original stack trace tốt hơn.
throw ex; không nên dùng để rethrow same exception.
3. finally dùng để làm gì?
Cleanup code cần chạy khi rời try, dù success hay exception.
4. Exception propagation là gì?
Exception đi ngược call stack tới khi gặp handler phù hợp.
5. Khi nào nên custom exception?
Khi cần một meaningful exceptional category có semantics rõ.
6. InnerException dùng để làm gì?
Preserve original/root cause khi wrap exception.
7. Tại sao không nên catch Exception everywhere?
Vì dễ swallow bugs, mất failure semantics và làm debugging khó.
8. Validation có nên dùng exception?
Thường expected validation errors phù hợp hơn với validation/result model.
9. Khi nào Result Pattern tốt hơn Exception?
Khi outcome là expected part của business flow và caller nên handle explicitly.
10. Tại sao không dùng exception làm normal control flow?
Intent khó đọc, cost cao hơn và dễ che business semantics.
11. Logging exception đúng cách?
Pass exception object vào logger cùng structured context.
12. Global exception handler dùng để làm gì?
Centralize logging/mapping exception thành safe HTTP response.
13. Tại sao không expose stack trace ra client?
Security và encapsulation; internal details nên nằm trong logs.
14. using liên quan exception thế nào?
Đảm bảo Dispose() khi scope kết thúc, kể cả khi có exception.
15. GC và Dispose() khác nhau thế nào?
GC reclaim managed memory; Dispose cleanup resource deterministically.
63. Bài Tập Ngày 9
Bài 1 — Basic try/catch
Viết:
int.Parse(input)
và catch:
FormatException
OverflowException
Giải thích vì sao specific catch tốt hơn catch-all.
Bài 2 — throw vs throw ex
Tạo:
MethodA
→ MethodB
→ MethodC
→ throw
Ở MethodB, thử:
throw;
và:
throw ex;
so sánh stack trace.
Bài 3 — Custom Exception
Tạo:
UserNotFoundException
có:
UserId
Message
throw từ service.
Bài 4 — InnerException
Giả lập:
Repository exception
↓
wrap thành Application exception
và preserve InnerException.
Bài 5 — Finally
Mở file bằng stream.
Throw exception trong processing.
Kiểm tra cleanup vẫn xảy ra.
Bài 6 — Using
Viết lại bài 5 bằng:
using
và giải thích vì sao code đơn giản hơn.
Bài 7 — Result vs Exception
Case:
Coupon expired
Viết hai version:
throw CouponExpiredException
và:
return Result.Expired
Sau đó so sánh trade-off.
Bài 8 — Global Handler
Thiết kế mapping:
ValidationException → 400
NotFoundException → 404
ConflictException → 409
Unexpected → 500
Bài 9 — Structured Logging
Log:
OrderId
UserId
Exception
TraceId
không log sensitive data.
Bài 10 — Catch Decision
Với từng case sau, quyết định:
catch?
propagate?
result?
retry?
Cases:
DB timeout
invalid email
duplicate username
null invariant
third-party 503
permission denied
64. Production Exercise
Thiết kế error flow cho endpoint:
POST /orders
Possible outcomes:
Invalid request
User not found
Insufficient balance
Duplicate request
Database timeout
Unexpected bug
Yêu cầu:
1. Phân loại expected vs exceptional
2. Chọn Result hay Exception
3. Chọn HTTP status
4. Chọn nơi log
5. Chọn case nào retry
6. Không expose technical stack trace
7. Preserve traceId
Key Takeaways
Exception
→ không chỉ là syntax try/catch
→ nó là một control-flow mechanism cho failure
Luôn hỏi:
Failure này expected hay unexpected?
Layer hiện tại có xử lý được không?
Catch để làm gì?
Có cần preserve InnerException không?
Có nên log ở đây không?
Có đang duplicate log không?
Có đang dùng exception cho normal flow không?
Client cần biết gì?
Log cần giữ gì?
Nếu trả lời được các câu này, bạn đã đi từ:
"biết dùng try/catch"
sang:
"biết thiết kế error handling cho production backend"