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ì?
  • throw khác throw ex ra 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, IDisposable liê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"