Avoid using failwith, assert false, and similar blunt error handling mechanisms that can turn recoverable errors into unrecoverable ones. Instead, prefer more robust approaches:

Use pattern matching over exception handling:

(* Avoid *)
let params = if is_method then
    let (lb, ps', rb) = def.G.fparams in
    (lb, (try List.tl ps' with Failure _ -> ps'), rb)

(* Prefer *)
let params = if is_method then
    let (lb, ps', rb) = def.G.fparams in
    match ps' with
    | [] -> (lb, ps', rb)
    | _ :: tl -> (lb, tl, rb)

Use Result types for error-prone operations:

(* Avoid Either.t, prefer Result.t *)
val load_rules_from_file : ... -> (rules_and_origin, Rule.error) Result.t

Choose appropriate exception types:

Handle dangerous operations safely:

(* Avoid *)
let repository_path = url |> Git_wrapper.temporary_remote_checkout_path |> Option.get

(* Prefer *)
match Git_wrapper.temporary_remote_checkout_path url with
| Some path -> path
| None -> Error.abort "Failed to create temporary checkout path"

This approach prevents crashes from supposedly “impossible” cases (especially after error recovery), allows for better error propagation, and makes code more maintainable by explicitly handling edge cases.