비동기 함수에서 throw한 예외가 try-catch에서 잡히지 않는 이유가 궁금합니다

안녕하세요.
아래와 같이 클래스 A에서 클래스 B의 비동기 함수를 try-catch로 감싸 호출했습니다.
클래스 B의 비동기 함수 내부에서 예외를 throw했는데,
예외가 try-catch에서 잡히지 않고 프로그램이 종료됩니다.
혹시 어떤 이유 때문인지, 그리고 어떻게 처리해야 하는지 조언 부탁드립니다.

감사합니다.

// 클래스B: 비동기 함수에서 예외 발생
public class B
{
    public async Task DoWorkAsync()
    {
        await Task.Run(() =>
        {
            throw new InvalidOperationException("예외 발생!");
        });
    }
}

// 클래스A: 비동기 함수 호출 및 try-catch
public class A
{
    private readonly B b = new B();

    public async Task RunAsync()
    {
        try
        {
            await b.DoWorkAsync();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"예외 잡음: {ex.Message}");
        }
    }
}
3개의 좋아요
var a = new A();
await a.RunAsync();

// 클래스B: 비동기 함수에서 예외 발생
public class B
{
    public async Task DoWorkAsync()
    {
        await Task.Run(() =>
        {
            throw new InvalidOperationException("예외 발생!");
        });
    }
}

// 클래스A: 비동기 함수 호출 및 try-catch
public class A
{
    private readonly B b = new B();

    public async Task RunAsync()
    {
        try
        {
            await b.DoWorkAsync();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"예외 잡음: {ex.Message}");
        }
    }
}

예외 발생이 잘 되는데… 샘플 프로젝트 만드셔서 전달 가능하실까요?
아, .NET 9 콘솔 프로젝트로 만든 것입니다.

1개의 좋아요

저도 잘만되는데…

1개의 좋아요

@Vincent 님께서 그제 올리신 질문과 비슷한 이유이지 않을까 싶은데요ㅎ

의문의 .NET 프로세스 종료. 디버깅 노하우를 구합니다. (MiniExcel) - :interrobang: 프로그래밍 언어 Q&A - 닷넷데브

await 없이 A.RunAsync()를 호출하신게 아닐지ㅎ

// static void Main()
new A().RunAsync(); // 예외 안잡힘
// static async Task Main()
await new A().RunAsync() // 예외 잡힘
3개의 좋아요

저도 방금 콘솔 창에선 잘 되는 것을 확인했지만, Winform 환경의 프로젝트 코드에서는 자꾸 에러를 못잡고, throw 하는 부분에서 자꾸 프로그램이 죽습니다… 혹시 UI 스레드나 다른 문제때문에 이렇게 프로그램이 계속 죽는 것일까요?

public async Task OnInitializeAsync()
{
    var filePath = _mainview.SelectedFilePath;

    if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
    {
        _mainview.ShowMessage("올바른 파일을 선택하세요.");
        return;
    }

    // 싱글톤 인스턴스 가져오기
    manager = Manager.Instance;

    try
    {
        // 비동기 초기화 호출
        await manager.InitializeAsync(filePath);
    }
    catch (Exception ex)
    {
        MessageBox.Show($"초기화 실패: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
        return;
    }
   
    LogMessage("Initialization");
}

public async Task InitializeAsync(string configFilePath)
{
    // 백그라운드 스레드에서 초기화 수행
    await Task.Run(() =>
    {
        resultCode |= SomeControlLibrary.InitializeFromFile(out handle, configFilePath);

        if (resultCode != 0)
        {
            SomeControlLibrary.Delete(handle);
            throw new InvalidOperationException("Initialization failed!");
        }

        initiator = new AutoStart(handle);

        resultCode |= SomeControlLibrary.RegisterCallbackJobReady(handle, initiator);

        monitor = new WaitForFinished();
        resultCode |= SomeControlLibrary.RegisterCallbackJobFinished(handle, monitor);
    });
}

분명 Catch로 잡고있는데 처리하지않은 예외라 뜨네요. 어떤 실수를 했을까요…?

예외처리의 MessageBox.Show 부분을

await this.InvokeAsync(() => {
MessageBox.Show($"초기화 실패: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
});

위처럼 바꿔보세요

await밖에서 catch하고 있는 경우 Task를 실행하고 있는 스레드의 호출 스택상으로는 catch하고 있는 부분이 없는 것으로 간주되기 때문에 사용자가 처리하지 않은 예외로 뜨는것은 정상입니다.

F5를 눌러 계속하면 죽나요?

1개의 좋아요

만약, 디버깅 모드 실행이고, 디버거가 예외를 보여주는 현상을 "프로그램이 종료"한다고 표현한 것이라면, [계속(F5)] 을 누르면 Catch 블럭으로 이동할 것이라 정상 동작입니다.

릴리스 모드 실행이라면 디버거 개입이 없으므로, 이러한 멈춤 현상이 없는 게 정상입니다. 그럼에도, 프로그램이 멈춘다면, 원인은 다른 곳에 있을 확률이 큽니다.

그리고, UI의 상태 변수를 다른 스레드와 공유하거나, UI 스레드에서 외부 변수를 직접 조작하면 그 스레드 혹은 의존 객체의 문제가 UI에게 전파되므로, 아래와 같이 격리하는 것이 비정상 종료를 예방하는데 도움이 됩니다.

public async IAsyncEnumerable<ResultCode> InitializeAsync(
        string configFilePath, 
        [EnumeratorCancellation] CancellationToken ct)
{    
   var (code, handle) = await Task.Run(() =>
   {
      ct.ThrowIfCancellationRequested();
      var code = SomeControlLibrary.InitializeFromFile(out var handle, configFilePath);
      return (code, handle);
   }, ct)
   .ConfigureAwait(false);  // 스레드 풀 스레드 사용
   
   if (code != 0)
   {
      SomeControlLibrary.Delete(handle);
      yield return ResultCode.InitializationFailed;
      yield break;
   }
   yield return code;

   yield return await Task.Run(() =>
   {
      ct.ThrowIfCancellationRequested();
      var initiator = new AutoStart(handle);
      return SomeControlLibrary.RegisterCallbackJobReady(handle, initiator);
   }, ct).ConfigureAwait(false);

   yield return await Task.Run(() =>
   {
       ct.ThrowIfCancellationRequested();
       var monitor = new WaitForFinished();
       return SomeControlLibrary.RegisterCallbackJobFinished(handle, monitor);
   }, ct).ConfigureAwait(false);
}

소비 코드

public async Task OnInitializeAsync()
{
   // ...

   using var cts = new CancellationTokenSource();

   EventHandler onCancel = (_, _) => cts.Cancel();
   _initCancelBtnEnabled = true;
   _initCancelBtn.Click += onCancel;
    
   try
   {
      await foreach (var code in manager.InitializeAsync(filePath , cts.Token))
      {
         resultCode |= code;
         if (code == ResultCode.InitializationFailed)
         {
            MessageBox.Show($"초기화 실패", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
            break;
         }
      }
      LogMessage("Initialization");
   }
   catch(OperationCancelledException)
   {
      resultCode = ResultCode.Cancelled;
      MessageBox.Show($"초기화 취소", "사용자", MessageBoxButtons.OK, MessageBoxIcon.Stop);
   }
   catch(// ...)
   {
      // 다른 예외 처리
   }
   finally
   {
       _initCancelBtnEnabled = false;
       _initCancelBtn.Click -= onCancel;
   }
}

주의 : 글쓰기 창에서 쓴 것이라, 오탈자 등의 이유로 동작하지 않을 수 있습니다.

이렇게 해보세요…

public async Task SelectDatabase()
{
DataSet ds = new DataSet();
List listUserTemp = new List();
Exception exectpion = null;

  Task t = Task.Run(() =>
  {
      try
      {
          using (SqlConnection sqlConnection = new SqlConnection(Properties.Settings.Default.ConnectionStr))
          {

              SqlDataAdapter sqlDataAdapter = new SqlDataAdapter();
              sqlDataAdapter.SelectCommand = new SqlCommand("SELECT * FROM USERINFO;",sqlConnection);
              sqlDataAdapter.Fill(ds);
          }

          if (ds.Tables.Count!=0)
          {
              DataTable dt = ds.Tables[0];

              for (int i = 0; i < dt.Rows.Count; i++)
              {
                  USERINFO userInfo = new USERINFO();
                  userInfo.USERNAME = dt.Rows[i]["USERNAME"].ToString();
                  userInfo.USERIMG = dt.Rows[i]["USERIMG"].ToString();
                  userInfo.USERAGE = Int32.Parse(dt.Rows[i]["USERAGE"].ToString());

                  listUserTemp.Add(userInfo);
              }

          }

      }
      catch (Exception ex)
      {
          exectpion = ex;
      }
  });

  await t;

  if (exectpion!=null)
  {
      MessageBox.Show(exectpion.Message.ToString());
  }

  MyListUser = listUserTemp;

}