If an function might fail during ‘normal operation’, it is better to return an ok/error-tuple to make it explicit that failures will commonly happen, to make the consumer of the code think of it.
If a function will only fail in exceptional circumstances, then it is usually better to raise an exception instead; these are the cases where ‘let it crash’ is the most appropriate default. The idea here is that these kinds of failures are implicit, which is okay since they are rare, so your consumer does not need to keep them in mind. In the rare, exceptional case that the consumer happens to trigger them (and triggers them so often that they become annoying), then they can mitigate them by rescuing them.
Do note that it is good form to list what kind of exceptions might be returned and why (both in the documentation as well as in the exception’s error message), so the user is able to find the information if they are looking for it.
In the case that a consumer of your code does not hold themselves to the preconditions of your function (such as passing in an argument of the wrong type), then you should immediately raise an error, such as an ArgumentError. After all, there is nothing sensible your function can do when it is improperly called. (It would be even better to refuse to compile the application, but this is not possible in a dynamic language, so raising at runtime immediately after calling it improperly is the best we can do.)
- Make your errors explicit by returning them as values when they will commonly be encountered.
- Leave rare, exceptional cases implicit in your return type by raising exceptions when they occur.
- When someone is not using your functions properly, immediately make them aware by raising an error.