What has really worked for me lately are these: Try::Tiny, Exception::Class, Exception::Caught, and Error::Return. These take advantage of the fact that you can die with an object. Therefore you can determine if what you have just “caught” is an instance of that particular error-class ... much better, as you have observed, than munching an error-string.
As far as “philosophy” goes, my general practice is that if a method finds that it is able to return a value, it does so. If not, it throws an exception. Therefore, if the method returns to its caller, it has something to say. The code that made the call doesn’t have to “check to see if it worked.” If you survived the call, it worked. I’m generally satisfied with the clean code that results from this approach.
Within my application, I actually define a package that contains nothing but exception-class definitions, each of them arranged into a hierarchy. Every exception that is thrown by the application code originates in this hierarchy, and somewhere there will be a “catcher of last resort” exception-handling block which will receive any exception that’s not otherwise trapped.