I've always liked the Haskell approach of using a monad like Maybe or Either. Maybe is a special case where rather than having an error you have a null--perfect for functions like indexOf--but since it is isomorphic to Either (), all my points apply to both.
The most important point is that in Haskell these errors are reified as first-class types. So the type `Either err val` is just like Go's method of returning either an error code or the correct result. However, rather than being a special case, this is just a normal algebraic data type. This is a more elegant way to support this sort of error-handling, as opposed to just baking it into the language explicitly.
Being first-class citizens is nice, and we'll get back to it, but first let's look at another way they're better than Go's approach--they form a monad. There is some rich theory behind this, but it also has an immediate practical benefit: you get the default action of propagating the error for free. That is, code like this:
if isError riskyValue
then error
else if isError (riskyFunction riskyValue)
then error
else ...
transforms into:
do value
so you get the simplicity of Go's approach with the convenient propagation semantics of normal exceptions. Now you only have to check for an error when you want to handle it; it gets sent through transparently if you don't. This is also cool because you can use the Either type to model early termination rather than an error, which is why it's called Either rather than Error.
So, being a monad, the Either type is nicer to use than normal returned error codes. But earlier I mentioned that there are some advantages to being a first-class citizen. What are these?
Well, the main advantage is that there is a fair number of generic functions that can be used to make your error-handling code neater. For example, you can use the alternation operator in a pattern I really like:
canError1 a b canError2 a b return 42
what this does is try the various options one by one until it either finds one that isn't an error or gets to the end of the expression. The last element could be a default result, as here, or a default error.
Another fun combinator is optional:
do someImportantAction 1 2
val
Another really cool thing is how types like this can interact with other types. In particular, I am thinking of monad transformers. In simple terms, monad transformers allow you to combine different "effects"; for example, you could combine error-handling as here with backtracking. But here you have two options: an error can either make the
entire backtracking computation fail or it can just make a
single branch fail. Which one should you choose?
The cool answer is that it is, indeed, the programmers choice. In particular, the order of transformers controls these semantics; something like EitherT err (Logic a) would be the first and LogicT (Either err a) would be the second. Not only do the types reflect the semantics, they actually control them! Very much self-documenting code.
The Haskell approach gives you high-level, declarative and very extensive control over exactly how you want to handle errors while hiding enough of the normal boilerplate to make them convenient to use. All this in a way that is not baked into the language but just an instance of a more general pattern (in this case a monad and monad transformer).