seems to be throwing a System.IO.IOException which is not being caught by a surrounding catch block
It is being handled, you only see the "first chance" notification, not an "Exception was unhandled" message. These exceptions occur on a worker thread that SerialPort starts to generate the DataReceived, ErrorReceived and PinChanged events.
You get this because you close the port while it is receiving data, setting DtrEnable = False can only work when you give the serial device time to see the signal change, you don't wait nearly long enough for that. DoEvents() is dangerous and doesn't fix it because it only delays for a ~microsecond, you need a second to be sure. Jerking the USB connector and then trying to close the port is another good way to trigger exceptions, there are very few USB device drivers that handle this smoothly.
and causing the program to lock up
This is your real problem. SerialPort.Close() will deadlock when you close the port while it is receiving data and made a mistake in your DataReceived event handler. The port cannot close until your event handler stops running. The standard mistake is using Control.Invoke()
in your event handler, typically used because you can't update the UI from a worker thread. But that's a very dangerous method, it is very apt to cause deadlock. It cannot complete until your UI thread goes idle. It isn't idle, it is stuck in the Close call. Which cannot complete until your event handler stops running. It won't stop running, it is stuck in the Invoke() call. Deadlock city.
Always use Control.BeginInvoke()
instead, it cannot cause deadlock. Calling Close() on another thread is a band-aid as well.