c++exceptionwinapimfchandler

Catch exception inside MFC callback function / event handlers


I have an MFC project with a modal dialog. At the begin of my code, I have a try/catch statement and I try to throw exceptions in various place of my code.

The exception from OnBnClickedButton1 or OnTvnGetdispinfoTree is handled properly, but the throw from OnTvnSelchangedTree is unhandled - see picture below.

It is a standard dialog-based MFC project generated by the Visual Studio 2022 wizard. On the dialog are a button and CTreeControl.

Main code with try/catch statement:

try {
  CMFCExceptDlg dlg;
  m_pMainWnd = &dlg;
  INT_PTR nResponse = dlg.DoModal();
  if (nResponse == IDOK)  {
     TRACE(traceAppMsg, 0, "OK...\n");
  }
  else if (nResponse == IDCANCEL)  {
     TRACE(traceAppMsg, 0, "Cancel...\n");
  }
  else if (nResponse == -1)  {
      TRACE(traceAppMsg, 0, "Warning: dialog creation failed, so application is 
     terminating unexpectedly.\n");
      TRACE(traceAppMsg, 0, "Warning: if you are using MFC controls on the dialog, you 
      cannot #define _AFX_NO_MFC_CONTROLS_IN_DIALOGS.\n");
  }   
 }    
 catch(std::exception &exc)   {
    const char *x = exc.what();
    TRACE(traceAppMsg, 0, exc.what());
 }

Dialog code (.h file):

class CMFCExceptDlg : public CDialogEx 
{
public:
    CMFCExceptDlg(CWnd* pParent = nullptr); // standard constructor

#ifdef AFX_DESIGN_TIME
    enum { IDD = IDD_MFC_EXCEPT_DIALOG };
#endif

protected:
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV support

protected:
    HICON m_hIcon;
    CTreeCtrl m_cTree;
    virtual BOOL OnInitDialog();
    DECLARE_MESSAGE_MAP()
public:
    afx_msg void OnTvnGetdispinfoTree(NMHDR *pNMHDR, LRESULT *pResult);
    afx_msg void OnBnClickedButton1();
    afx_msg void OnTvnSelchangedTree(NMHDR* pNMHDR, LRESULT* pResult);
};

Implementation of dialog (.cpp):

CMFCExceptDlg::CMFCExceptDlg(CWnd* pParent /*=nullptr*/)
: CDialogEx(IDD_MFC_EXCEPT_DIALOG, pParent)
{
    m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

void CMFCExceptDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialogEx::DoDataExchange(pDX);
    DDX_Control(pDX, IDC_TREE, m_cTree);
}

BEGIN_MESSAGE_MAP(CMFCExceptDlg, CDialogEx)

    ON_NOTIFY(TVN_GETDISPINFO, IDC_TREE, &CMFCExceptDlg::OnTvnGetdispinfoTree)
    ON_BN_CLICKED(IDC_BUTTON1, &CMFCExceptDlg::OnBnClickedButton1)
    ON_NOTIFY(TVN_SELCHANGED, IDC_TREE, &CMFCExceptDlg::OnTvnSelchangedTree)
END_MESSAGE_MAP()

// CMFCExceptDlg message handlers
BOOL CMFCExceptDlg::OnInitDialog()
{
    CDialogEx::OnInitDialog();

    m_cTree.InsertItem(LPSTR_TEXTCALLBACK, TVI_ROOT);
    m_cTree.InsertItem(LPSTR_TEXTCALLBACK, TVI_ROOT);
    m_cTree.InsertItem(LPSTR_TEXTCALLBACK, TVI_ROOT);
    m_cTree.InsertItem(LPSTR_TEXTCALLBACK, TVI_ROOT);

    return TRUE;  
}

void CMFCExceptDlg::OnTvnGetdispinfoTree(NMHDR *pNMHDR, LRESULT *pResult)
{
    LPNMTVDISPINFO pTVDispInfo = reinterpret_cast<LPNMTVDISPINFO>(pNMHDR);
    *pResult = 0;
    throw std::exception("GetDispInfo");
}

void CMFCExceptDlg::OnBnClickedButton1()
{
    throw std::exception("In Click Button Error");
}

void CMFCExceptDlg::OnTvnSelchangedTree(NMHDR* pNMHDR, LRESULT* pResult)
{
    LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);
    // TODO: Add your control notification handler code here
    *pResult = 0;
    throw std::exception("OnTvnSelchangedTree");
}

The throw from this last handler (SelChanged) doesn't go to the catch statement, but finishes by an error instead:

Unhandled exception


Solution

  • You cannot throw (C++) exceptions across foreign stack frames1. The TVN_SELCHANGED notification is sent by the tree-view control to its parent, allowing it to respond to changes in UI state. When the callback returns, control crosses back into the control implementation.

    The control isn't prepared for C++ exceptions. For all we know, it could be implemented in C and doesn't even know what C++ exceptions are. It is thus crucial that C++ exceptions never escape your window procedure implementation.

    The easiest way to ensure this doesn't happen2 is by applying the noexcept specifier to the window procedure. Since this is MFC you don't control the window procedure and instead have to mark all message handlers as noexcept. With that in place, any attempt to throw a C++ exception across a message handler will have the language runtime initiate a controlled emergency shutdown.

    Out of all outcomes, this is the best you can hope for.

    If you need to collect information when this happens, you can have your installer set up the system to collect user-mode dumps, allowing you to perform post-mortem analysis of what could well be a bug.


    A note on TVN_SELCHANGED specifically: This is a notification meant for observation only. If the parent decides to handle this notification, the handler's return value is expressly ignored. It seems rather unusual to want to scream "Oh noes!" when no one is listening anyway.

    If you wish to cancel selection, you need to respond to the TVN_SELCHANGING message instead. Its return value is observed and controls whether the selection change is allowed.


    1 Details in Raymond Chen's blog post When you transfer control across stack frames, all the frames in between need to be in on the joke.

    2 An alternative would be a function-try-block. This is more involved as you have to ensure you aren't "swallowing" exceptions you didn't expect.