There is no way to use catch in pure code. This is by design: exceptions are handled by the IO system. That's why the type of catch uses IO. If you want to handle failure in pure code, you should use a type to represent the possibility of failure. In this case, the failure is that the value can sometimes not exist.
The type we use in Haskell to denote a value that can either exist or not is called Maybe. Maybe is an instance of Monad, so it is "monadic", but that should not dissuade you from using it for its intended purpose. So the function you want is headMay from the safe package: headMay :: [a] -> Maybe a.
That said, if you want to avoid monads you can instead use a function that unpacks the list:
listElim :: b -> (a -> [a] -> b) -> [a] -> b
listElim nil _ [] = nil
listElim _ cons (x:xs) = cons x xs
As you can see, this replaces a [] with nil and a : with a call to cons. Now you can write a safe head that lets you specify a default:
headDef :: a -> [a] -> a
headDef def = listElim def const
Unfortunately, functions are an instance of Monad, so it turns out that you have been "force[d] to be monadic" after all! There is truly no escaping the monad, so perhaps it is better to learn how to use them productively instead.