- Hi, now the next step in learning design is error handling
We started very early in one of the very first sections,
when I was trying to prepare you for this class,
to tell you that lines of code are important
and to tell you that if you really wanna maintain
mental models and reduce the chances of legacy code,
you've got to worry about failure.
Failure is always what we're coding against
and error handling is everything.
It's not about exception handling,
because exception handling I think
hides context around errors.
This is about looking at an error
when it's happening, right, and Go errors are just values
and they can be anything you need them to be.
But from an API design perspective,
for me error handling is about one thing,
it's about showing respect to the user of your API,
giving your user enough context to make an informed decision
about the state of the application
and giving them enough information to be able to either
recover or make a decision to shut down.
If your application loses integrity,
you have a responsibility to shut it down.
I don't wanna shut down apps, but you have a responsibility,
because integrity, data corruption,
no no no no no no no no no.
And there's two ways to shut down an application in Go.
You can go to OS, the OS package, dot exit,
and you can set a return code on that,
that's the fastest way,
or you can call the built in function panic.
Now you'll choose one over the other depending on if
you need a stack trace or not.
So if you need the stack trace you're gonna call panic,
if you don't you just call OS exit.
But we've gotta design APIs and errors around the idea
of giving the user enough context
to make an informed decision.
So let's start with the basic mechanics
and what the language provides you,
in terms of error handling.
Now what you're gonna see here is the error interface.
Now this error interface is provided to us,
built in to the language.
There really is no package called built in here,
this URL, that name built in
is there purely for documentation.
But look at the error interface,
it looks like it's un-exported, but it really isn't
because again it's built into the language,
and it's got one active behavior, error,
and it returns a string
and this interface is how we work with errors.
If you think about it, error handling in Go
is done from a decoupled state
and that's very very important
because error handling is gonna be all over the place
because it's gonna be wide spread,
we want it decoupled so we can change error handling,
improve and refactor error handling without creating
a large set of cascading changes throughout a code base.
So this interface is critical,
but again error are just values in Go
and the most widely used value for error handling
is what you see on line 15 and that is the error string type
and this comes from the errors package.
Notice a couple things about the error string package,
one it's a struct type that is un-exported
and two it has one field in there
which is an un-exported field named S of type string.
This is the most commonly used error value in Go
and in many many cases gives us enough context
to give the caller enough information
to make an informed decision.
Now the next thing you're gonna see from the errors package,
implementation of the error interface.
I want you to notice a couple things here,
one choosing pointer semantics
and that's very very important,
and we're gonna come back and explain
why pointer semantics are very important here.
Unless you're working with a errored value type
that is gonna require value semantics,
maybe like a reference type or a built in type,
if we're using that struct I'd most definitely want
pointer semantics for the implementation
of the error interface.
And notice that it's really just there for one purpose,
one to make the value compliant with errors,
but two for logging.
Whatever comes out of this method should be
for logging and logging only.
If your user has to parse the string coming out,
you have failed the user.
We can not have them parsing to get context.
Okay, now, now that we have the implementation
of the interface, we now have the factory function,
there it is, again coming from the errors package.
And this is how we create errors.
And remember or, more importantly look at
the return of this factory, it is the error interface.
And we look on line 27 and what we see is that we are
constructing a value of the un-exported type error string,
taking it's address, because we're using pointer semantics
for the implementation, which means that we can only share
and we stick that in the error.
So any time you call new, I wanna show you
on the white board for a second what that error variable
is gonna look like when we call new,
because what's interesting here is we've got
a factory function that's not returning
a concrete value directly, it's returning
an interface value and remember I told you
that factory functions really should be returning
concrete values and pointers.
Except this is that one kind of exception,
remember there's exceptions to everything
and that's new, because you always work with errors
from an interface perspective.
So when I call new, what we're gonna end up
really getting back is the error interface value.
It's gonna say I have a pointer to an error string
and it's gonna have our pointer to the struct
error string type, which is really just a string
at the end of the day.
Well it probably wouldn't say Bill, right?
It would be some sort of error message, whatever that is.
And this is what we're gonna get back,
we're gonna get errors and work with errors
from a decoupled state.
This is now an error interface value with a concrete pointer
inside of it to our error message or string.
And it's very typical in Go to be looking
at this kind of code here when it comes to error handling.
Notice that we're in an if statement,
inside the if statement we make a call
to say in this case webCall,
we check to see if webCall has failed,
we've got the error not equals nil.
Now there's a few things here that I wanna talk about
before we move on.
One is that people liked the idea of exception handling
because the idea was the try code had your happy path
and your catch took care of all the negative path
and you separated this for code readability.
I get it, but I can't tell you how many times
when I landed in the catch, I was lost,
because the context of how I got there is lost.
Was it this call, was it that call?
And I used to spend too much time trying to figure out
how I got inside of a catch, it doesn't work.
But you can still have this concept of happy path
if you follow this basic design principle
when writing code in Go.
And when it is reasonable and practical,
what I want you to do, is use the if statement
to handle your negative path logic and keep your path,
your positive path logic on a straight line of sight.
In other words, if this call to webCall succeeds,
then I just move down the function to the next call.
So this line of sight right here, this tab,
is my happy path, I'm gonna get the same benefits
we were getting from exception handling.
If this call was succeeded, no problem,
we keep our line of sight going and we continue to go down
this line of sight for the happy path
and we use the if statement to deal with
our negative path logic.
My line of sight down this line right here
is our negative path and the idea is
that we return out of the negative path.
So we do something if there's failure then
we're gonna handle that failure, whatever that means,
we'll talk about that and then you will return.
You can have, even though you have a single entry point
for a function, get into your head
that we can have multiple exit points.
And the one thing I really want you to avoid using
is an else clause, else clauses make code
an order of magnitude more complicated to read,
because what else clauses typically try to do
is you're putting positive path logic inside your if,
you're trying to push a function
from the beginning all the way down.
It just means I have to consume, read,
and understand more code as I go through the function.
When you use this pattern I don't have to worry
about reading as much code because I know that
if I've gotten here and I've moved down to line 36,
I know everything above is already done
and fine and we're good.
So I really want you to follow this pattern.
I also want you to stay away from else clauses,
because we're not looking to have a single exit point.
What's really nice about Go too is they have this concept
of a naked switch and on the naked switch,
in your case you could do Boolean logic,
you can write Boolean logic here
and if you do have a situation where you may have
an if else, if else, then use the switch to do that
and again leverage the idea of negative path
on the return, negative path always indented.
Try these lines of, lines of you know scope here,
these lines of path, you'll see that it's gonna
really help you and as we continue to work with code here
I'll show you how this code base is doing it.
Okay great, so, we've got the call to webCall.
WebCall down here is returning an error,
it's returning an error interface value
where the string inside of our error value here
says bad request, right that's the string,
bad request, so when we call webCall
we take that error interface value,
we store it in a local variable to the if called error,
we're using our short variable declaration operatives,
it's a local variable only bound to the if,
this is really great and then we check
error not equal to nil.
Now this is interesting, 'cause nil
is really an interesting concept in Go.
Nil is always the zero value for the two types of
types that can be nil, one is pointers
and one are the reference types.
Alright, so nil always takes on
the type it needs to satisfy an expression.
In this case what is nil?
Well since err is an error interface value,
then what nil represents is a nil error interface value.
You see, it's always gonna take on the type it needs,
so we can do the compare.
But I like English and so for me, error not equals nil,
what it really means, it's asking one question,
it's asking, is there a concrete value stored
inside that error interface.
Here's a situation where there's no concrete value stored,
here's a situation where there is
and in our case we're going to have it.
So anytime I see error not equals nil,
what we're really saying is, is there a value,
whether a piece of data, a value or pointer,
is there something stored concrete inside
the error interface and if there is,
that means we have an error.
Now in this case the fact that we have an error
is all the context we need,
because this function only returns one error
and we know what that error is and we have enough context
so we can make an informed decision
about how to handle the error.
All I'm doing right now is logging it and returning it.
Alright, so when you got a function that returns
just one error, a lot of times it's just the fact
that an error exists, that there's a concrete value
stored inside the error interface, which is enough.
But what if webCall could return multiple errors?
At that point now we have to deal with a different mechanism
to give the user enough context
to make an informed decision.