Don’t try to read too much in the stdlib, it’s too old to be fully consistent. But a cue between the difference on array accessors and List.hd can be given by the -unsafe compiler option.
Nowadays List.hd could be List.hd_opt, (cue List.nth vs List.nth_opt). But the same treatment would not happen on Array.get.
A good rule of thumb is to use invalid_arg for erroring on invariants whose duty is for the client to maintain before calling the function. Notably for get_* accessors.
There was a time where result did not exist and Failure msg served the purpose of Error msg – and for some people still does.
Personally I most of the time don’t let exception cross API boundaries, however I still find Failure useful, for example to easily make internal functions that eventually return a result tail recursive by avoiding a bind on each recursive call.
let f x =
let loop … = … in
try Ok (loop …) with Failure msg -> Error msg
Yes Failure _ must be caught if you can trigger it. It’s the equivalent of the Error _ case in result values.