37. async void vs async Task: 예외 전파와 완료 추적
난이도 상내 리뷰 · C#
해설 · C#
해설 — async void vs async Task: 예외 전파와 완료 추적
난이도: 상
요약
async void 는 호출자에게 돌려줄 Task 가 없다. 그래서 (1) 호출부의 try/catch 는 그 예외를 잡지 못하고, (2) 예외는 메서드가 시작될 때 캡처한 SynchronizationContext(없으면 스레드풀)로 다시 던져지며, (3) 호출자는 완료 시점도 알 수 없다. 반면 async Task 는 예외를 반환된 Task 에 저장했다가 await 지점에서 원래 예외로 다시 던지므로 정상적으로 잡힌다.
예측: "caught void" 는 출력되지 않고, "caught task: boom from async Task" 만 출력된다. FireAndForget 의 예외는 스레드풀로 올라가 콘솔 앱에서는 처리되지 않은 예외로 프로세스를 종료시킬 수 있다(타이밍에 따라 "Main done" 출력 전/후로 갈릴 수 있음).
핵심 개념
- async 메서드는 첫
await에서 호출자에게 반환한다.FireAndForget()호출은await Task.Yield()를 만나는 순간 즉시 반환하므로,(A)의try블록은 이미 정상적으로 빠져나온 뒤다. 이후 스레드풀에서 재개돼 던진 예외는 그try와 아무 관계가 없다. - 예외 저장 위치:
async Task→ 반환Task.Exception에 저장 →await가 언래핑해 재던짐.async void→ 저장할 Task 가 없어 캡처된SynchronizationContext.Post로 올라감(콘솔/서버는 보통 컨텍스트가 null → 스레드풀에서 재던짐 →AppDomain.UnhandledException→ 프로세스 종료).
단계별 내부 동작
FireAndForget()진입 →Task.Yield()에서 상태 머신이 continuation 을 예약하고 void 반환.(A)의 try 정상 종료(예외 없음). "caught void" 는 절대 실행되지 않는다.(B)await FireTask():FireTask도 yield 에서 반환하지만Task를 돌려준다. 그 Task 가 예외로 완료되면await가 원래InvalidOperationException을 다시 던진다 → catch 가 잡아 "caught task: boom from async Task" 출력.- 한편 1에서 예약된 continuation 이 스레드풀에서 재개돼
throw→ 잡아줄 곳이 없는 async void 예외 → 처리되지 않은 예외로 상승. 콘솔 앱이면 프로세스 크래시.(A)의 catch 와는 무관. - "Main done" 은 프로세스가 크래시하기 전이면 출력될 수 있으나, async void 예외 처리 시점과 경합한다(비결정적).
흔한 오해·함정
- "try/catch 로 감쌌으니 안전하다"는 착각: async void 호출을 감싼 try/catch 는 동기 부분(첫 await 이전)만 보호한다. await 이후 예외는 못 잡는다.
- 완료 추적 불가: async void 는
await/Task.WhenAll대상이 될 수 없어, 호출자가 "끝났는지" 알 수 없다. 서버에서 fire-and-forget 작업을 async void 로 던지면 종료 시점에 진행 중 작업이 유실되거나 예외가 프로세스를 죽인다. - 유일한 정당한 용도는 이벤트 핸들러(시그니처가
void를 요구). 그 경우에도 핸들러 본문 전체를 try/catch 로 감싸 예외가 밖으로 새지 않게 해야 한다.
면접 포인트
- 왜 async void 를 피하는가: 예외를 호출부에서 못 잡고 프로세스를 죽일 수 있으며, 완료를 await 할 수 없다. 라이브러리/서버 코드는 항상
async Task(또는Task<T>)를 반환. - 부득이한 async void(이벤트 핸들러)의 안전장치: 본문 전체 try/catch, 또는
Func<Task>를 받아 안전하게 실행하는 래퍼. - async 메서드가 첫 await 에서 반환한다는 점, 예외가 Task 에 저장됐다 await 에서 재던져지는 메커니즘(ExceptionDispatchInfo 로 스택 보존)을 설명할 수 있어야 한다.