Go’s Explicit Error Handling Model
Go treats errors as values and requires developers to handle them immediately using if err != nil. This avoids hidden control flow and makes failures explicit, forcing conscious decisions about how to proceed.
Returning Errors to the Appropriate Layer
Lower-level packages (e.g., database layers) should return errors rather than handle them when they lack sufficient context. Higher-level layers (e.g., API layer) are better suited to decide whether to retry, log, or return user-facing messages.
Logging Strategy and Observability
Errors should be logged where they are handled, not where they originate. Logging at the handling layer prevents duplicate logs and keeps observability clear, while allowing different log levels depending on severity.
Custom Errors and Error Inspection
Developers can define custom errors and use errors.Is to compare specific error values. This enables structured branching logic, such as distinguishing between connection failures and duplicate records.
Error Wrapping and Joining
Using errors.Join, multiple errors can be combined to preserve context from different layers. This allows propagation of both high-level and low-level error information without losing detail.
Using errors.As for Error Trees errors.As enables matching specific error types within a chain of wrapped errors. This is useful when multiple errors are combined and specific handling is required based on underlying error types.
Decoupling from External Packages
Creating application-level error definitions prevents tight coupling to third-party packages. If an underlying library changes its error types, only localized logic needs adjustment rather than widespread refactoring.
Best Practices for Professional Error Handling
Handle errors where meaningful action can be taken, log strategically, provide clear user-facing messages, and maintain internal diagnostic detail. Mastery of error handling requires practice and thoughtful design.
In episode two, where we talked about if statements and switch statements,
I briefly used an error in the shorthand notation form of an if statement,
but I didn't fully talk about what they are and how they work.
Even though it should be pretty clear from the name what an error is,
they are somewhat different or somewhat unique compared to other programming languages.
So you might have seen this one before, if error does not equal nil, then we do something.
You also might have seen people on Hacker News, Reddit, Twitter, whatever,
hate on the amount of time that you need to write this statement.
So let me break down why this is actually good,
what errors are and what errors as values mean,
so you have a proper mental model of how to,
how to think about them.
First, I want to tackle this if error equals nil debacle,
because Go forces you to deal with errors when they occur and not later on in your,
in your program through some like catch-all function.
This is a great thing.
This is good.
It makes it obvious where something went wrong and requires a conscient decision as to what to do next.
Often, you just,
you turn the error and let the caller deal with it.
So I want to give you an example of this.
So say we have a API package here and I'm just going to call this one API.go API,
and in here we have something like func new user.
It just takes in an argument of name.
That's a string and returns an error and then we are going to call the database.
We're going to say error equals database
in
search user.
Now, this is obviously pseudo code.
We're going to have a lot of errors here.
Um, so just stick with me.
I'm also going to create like a pseudo database package here,
database and it's just called one DB that go packets,
database and we have a function here called insert user that takes in a name and returns an error.
And then we have some code for,
for executing SQL against the DB and for this example,
I'm just going to return an error so I can illustrate the concept.
So we just say could not insert record.
Now I can go back to API and we can actually import this and we see we have this this error right here.
So first of all,
where should we handle this error?
The reason why?
Our database insert user function could could error is multiple,
but we don't or we can't really do much in the database packets to fix it in a logical way.
It makes a lot more sense to deal with this in the API layer where we can return like a message to the user.
We can or we can retry if we want to write.
So for the database insert function,
this insert function here,
it makes a lot more sense that we're going to return it to the caller,
which in this case is the new user function in the API layer.
So as I mentioned,
the new user function could return a helpful message to the to the user.
It could issue a retry.
But how would we as the programmers know that an error has occurred?
And this is where we have observability or logging.
Go has a package,
it's in the in the standard library called Slug,
which gives us different options for outputting information that is not available to the user.
So that sounds great.
But where should we where should we log?
Because we have two places where we we have the same error in a sense,
right?
A good rule of thumb is that we we lock something where we handle it.
So in this case,
we just argued that in the database package here,
we can't really do much about this error.
We can only really return it.
So the only place we can realistically handle this error is inside this new user function.
And how we go about it.
Well, as I said,
we have multiple options,
but it makes sense that we handle this error here.
So if we were to say if error does not equal nil,
then we can return a new error here saying could,
not create a new user.
Please try again.
And then we import this errors package from the standard library as well.
Now, it's important that we are strategic about where we where we log.
Because if we lock too much,
the information is going to get lost in a lot of luck.
Right?
So here we could simply say after we return this message to the user slug info,
then we say an error occurred while inserting user in in database,
we have some context and then we say the error was error.
All right.
So here this slug.info never gets outputted to the user.
It only something that get outputted to us as the programmers and the maintainers.
And then the user will see this could not create new user.
Please try again message.
Now, if you were to lock this as well in the in the database,
you can see how we kind of will have the same the same lock twice.
Right. So we have even more information.
We need to pass through determine where something happened here is very clear that we called this.
Function from the database package.
We were trying to make an insert and something went wrong and then we have to error.
We can go we can go into the package and then fix that error.
I said earlier that errors are values,
which means that we can examine the type of error by looking at its value.
Now say this error we get right here is related to I don't know database connections.
This is always not the fault of the users but but something that we the developers
should fix in that case.
We can actually change the what is known as the lock level to go from info to to error
because this is an actual error that we need to look into right?
But how do we know how do we know that that this error is related to connections?
Well, if you were to go into insert user here, we could create a new variable say error connection.
And say errors new the data base connection was I don't know it was dropped right?
So this is our own our own custom error.
But we would typically also use a.
Package or something that connects or actually interfaces with the actual database.
So if you were to mimic this with some more pseudo code here,
we could say error equals let's just say we have a DB package in here right?
DB and that DB package has an exec function that lets us run some SQL.
So insert into users name values.
It's going to be just dollar sign one because we are going to be mimicking Postgres.
And then the name now our error handling here.
We can actually look at the specific type of error.
So if error is not nil, we can use a switch here and say I want you to spell switch correctly switch.
There we go and say let's say care case error then we can use the error is which just checked.
Exactly what the error is.
So if error is let's say we have a DB.
Connection error that case we return.
Error.
Connection failure.
We might have other errors as worldwide.
You could also have a couple paste this one and say we have a record exist.
And saying something like our matching.
Matching records already exist right then again.
You can say that all in this case like it is figuratively case here that our DB package has our this is record exists.
Now return our record exists.
There we go.
And if it's none of these.
Errors we just return we just return nil great.
Now we can go back into our API package and then we can see pretty much the same thing as before.
We said switch and now if error case error is database error.
We have connection failure then we can return.
A more specific error message to the user something like an error or code on our end.
Please try try again.
There we go.
And we could also since we now have the option to look at if the if the data that the user provided was bad because he was is error database error record exist.
And now.
We can return.
A more specific error message for that case.
So our user the same with that.
Name already exist.
Right.
And even more now we can also adjust the locks that we output.
So here we would clearly have an error occurred.
Because this is something on our end.
Now you can argue if you want to add a lock here saying.
A user tried to insert.
Record that already.
X already existed existed.
There we go.
Whether or not you want to do this that's up to you.
But now you have the option and you can do more.
Details or more correct outputs of whatever went wrong both to use as the programmer but also to the user.
And then we can just have our catch all down here.
Where we could not create the user please please try again.
Now as already mentioned we are using a figurative error package in this case to mimic the behavior of a real database package.
And I know we have database twice here but but stick with me because we're just trying I'm just trying to illustrate the concept of.
Of how you can you can deal with errors at different levels.
But say that this DB package here.
Also output today we can see we can see they have like concrete errors right like connection Eric record exists.
Error and in that case we might want to bring that information up to the higher level to give us more information.
In that case we can use this errors join.
Where we can add an error and then we can add our own error.
Multiple errors to this to this error chain or our tree.
And then here.
Do the same with our record exists so we can provide more context if we actually wanted to.
This is a really helpful behavior so you can get like the full picture of all the errors that occurred if you ever need to.
Great.
This might look a bit unnecessary and this is definitely a thought.
Up example we are going to deal with it differently in the in the actual course.
But say that we were to change the Israel packet the packets that we use for executing Israel against the database right.
That package might have different errors.
And if you start to rely on these errors well if you change that package we break pretty much everything.
But by creating our own errors here we can rely on those errors and then.
Just switches around in this specific function.
If there is anything to switch if the errors changes.
Now we made this join so we always have the context of our own errors but also this more specific errors of the of the package.
What we need to do now is we need to use what is known as errors as instead of is errors as we look for the first.
Error in the error tree that matches the error provided here and will only return true if it finds that error so we might have a ton of errors in this error chain or our tree but we know that okay we we we know that this error can happen and if that error happens you want to do something specific we still have everything that happened in this value right here so different this will have everything that happened up to this point and then we can examine the output.
That error in our in our locks right but we know that if it contains error connection failure then then we need to return this this error message right so.
Errors as is it's a really really good tool if you have multiple errors I was is is really good if you only have one hour.
This all different takes a bit of practice and these examples I've shown you right here is not really real world examples.
This is more.
Illustrative but.
Follow follow the guidelines here of trying to handle the error where it's.
Or trying to lock the error where it's handled and try to handle it in a place where it makes sense like and then again that takes that takes practice right because whether something makes sense makes sense to handle you could argue that makes sense to handle in the database.
But it also depends on if we can if the if the.
If the function can do something right and in the case of a record exist or connection failure we don't know what to do because we can fix it in in the database layer we can fix it in the API layer.
You can only really return a message here saying like this record exist or let us the programmers know that there's a connection failure and we need to look into how our database is configured.
So this is a very short intro to two errors we will use them a lot throughout the.
The development of this block so you will get to become very familiar with how how you can work with errors in a in a professional way.