Even in cases where the error does already precisely match the semantics of the function, you still need documentation. std::sync::Mutex::lock() returns a `Result<MutexGuard<T>, PoisonError<MutexGuard<T>>>`. What's a PoisonError? That's a precise error type for the semantics of this function, but you need the documentation to tell you what it actually means for a mutex to be poisoned.
You cannot get away from having documentation. And you're free to make custom error types for all your functions if you want to, it just doesn't really get you much benefit over having a single unified error type for your module in most cases. If you have a reason to believe the caller actually does want to handle error variants differently then sure, make a new error type with just the variants that apply to this function, there's plenty of precedent for that (e.g. tokio::sync::mpsc::Sender has distinct error types for send() and try_send(), because that's a case where the caller actually may care about the precise reason), but most error values in Rust just end up getting propagated upwards and then eventually logged.