Congrats on your first HN post! This place can be intimidating at first, but people here are generally very helpful.

As for your question: In what language? What ecosystem?

Different languages have different error handling paradigms, and "customs" (aka idiomatic code). Some languages have even changed slightly over the years.
It also depends on other, existing methods in the API, if it's an existing one.

Try to be consistent and not to surprise your users.

It also depends on what you mean by an "API". The original meaning of this phrase (calling inside the boundary of a single executable, or between applications using a more or less invisible RPC mechanism) has largely changed in recent years to mean Web APIs. Most answers around me allude to this.

My own take is below. My experience is mainly with C, C++ and C#, with some dabbling with Haskell, D, Lisp, Rust, and JavaScript.

1) Never throw exceptions, except under exceptional circumstances, when your program should crash anyway since you're in a point in the code in which you have no idea how to continue. I like Rust's approach that where there aren't any catch-able exceptions, only panics that always crash the application.

2) In particular, don't throw exceptions for bad user input, bad data from a file or database, error codes from another website or API. You know and you should handle them inside your API. Don't let exceptions from lower-levels APIs you use propagate upward. They don't mean anything to the caller and effectively expose your internal implementation.

3) It might be ok to throw exceptions inside your method if it makes your life a bit easier, if you catch them at some topmost level and return an error. It depends on the language you use.

4) A crashing application should create a dump file, which make analyzing errors in production much easier and quicker. This doesn't concern on you as the writer of a lower-level API, but it something you should raise with your direct or indirect consumers.

5) A slightly better way is to use return codes. But it's still not very good. In C for example that's about the only thing you have, except a global error code and that's even worse.

6) A better way is to use Option/Result types, but not all languages will have them and if they do they might not be idiomatic, and some of your users will suffer since you force upon them an unfamiliar paradigm and/or external dependencies. Also even Option/Result type often make the calling code devolve into if/else chains, unless the language has support for monadic chaining, and while you can find it, it's not idiomatic in many languages (yet?). Also few languages will force your callers to handle those returned errors.

7) That said, exceptions are your friends during development and also after - throw them when you reach a point in the code that you know you should never reach. This is if you can't make the error situation somehow fail compilation.

8) As for logging - yes you should log each error. My take on logging - if it's a lower level API code, it should never do logging by itself, unless it will always be a part of larger framework that already has an integrated logging. What I usually do is accept an optional logging callback to let the caller implement the logging.