Sobre o "não basta transformar qualquer exceção em horário ocupado": dá pra deixar isso objetivo olhando o SQLSTATE e o nome da restrição, nunca a mensagem:
23505(unique_violation) vindo deuma_reserva_ativa_por_vagavira 409 (no índice parcial, oconstraint_namedo erro traz o nome do próprio índice)23P01(exclusion_violation) vindo da restrição de sobreposição vira 409- o resto continua 500 (fora o
40P01abaixo)
Um detalhe do EXCLUDE que pouca gente conhece: diferente do índice único, ele grava a entrada no índice e só depois varre procurando conflito. Se duas transações inserirem quase juntas, pode acontecer de cada uma enxergar a linha não commitada da outra: as duas ficam esperando e o detector de deadlock aborta uma com 40P01, não 23P01. Vale tratar com retry da transação inteira (ela já foi abortada): na nova tentativa, a reexecução espera a outra terminar e cai no 23P01 normal (ou passa, se a outra deu rollback).
Pra testar sem sleep fixo: duas conexões, faz o insert na A sem commitar e dispara o da B em outra thread, que fica bloqueada esperando a A terminar. Faz polling até pg_blocking_pids(pid_da_b) conter o pid da A, commita a A e confere que a B recebeu 23505 e que existe exatamente uma reserva ativa. Repetindo com ROLLBACK na A, a B tem que passar.