When an interaction or startup path can fail (or an expected event never arrives), make error handling deterministic and user-safe: clean up reliably, reuse the same validation+error messaging, recover on the right signal, and replace (not just cover) UI that could trap focus.
Apply this checklist: 1) Deterministic cleanup for missing events
Example pattern:
function startDrag({ clientY }: { clientY: number }) {
const startHeight = height;
const previousCursor = document.body.style.cursor;
const previousUserSelect = document.body.style.userSelect;
document.body.style.cursor = 'row-resize';
document.body.style.userSelect = 'none';
const end = () => {
document.body.style.cursor = previousCursor;
document.body.style.userSelect = previousUserSelect;
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerup', end);
window.removeEventListener('pointercancel', end);
};
const onMove = (move: PointerEvent) => {
// Cover edge cases: release outside window may skip pointerup.
if ((move.buttons ?? 0) === 0) return end();
onHeightChange(clampTerminalPanelHeight(startHeight + (clientY - move.clientY), window.innerHeight));
};
window.addEventListener('pointermove', onMove);
window.addEventListener('pointerup', end);
window.addEventListener('pointercancel', end);
}
2) Reuse the same “can send/can’t send” checks on every action path
3) Retry startup/environment lookups on the real recovery boundary
4) On later failures, replace the UI state to prevent trapping