Now that we had a look behind the scenes
and had a look at the more difficult parts of JavaScript,
let's come back to something simpler.
Let's come back to error handling.
For this here,
I'm back in that project we worked on before in Express.
And you might remember that there, something could go wrong.
If we try to read or write to this restaurants.json file,
and I rename it
to simulate that it's currently not available,
then we got an error.
And we handled that
with that default error-handling middleware here
which allowed us to send back this 500 status code
and this 500 page.
That's what we saw before in this course.
Now, that was a really useful pattern
because it allowed us to ensure
that we sent back some response
even if something went wrong on our server.
But this is not necessarily always the best way
of handling such scenarios.
It was fine here, but the more complex your code gets,
the more actions you might want to execute
in your route handlers,
the less optimal this approach could get.
And therefore, I'm back in that simpler project
or this simpler demo workspace
we worked in this course section.
Here I'll add a new file, errors.js.
And in there I will write some code
that will only run on Node.js now
because it makes it a bit easier
for me to show you what I mean,
though the error-handling approach I'm about to show you
would also work on the frontend, in the browser side.
Now, here I'll again require the file system package,
and that's the part that only works here in Node.js
because that is not code that would work in the browser.
The browser would neither understand this require function
nor do you have access to some file system in the browser.
But the rest of the code which I'll write
would work the same way in the browser
if we leave out the file system part.
And here I'll add a new function, readFile,
and in that function I will now call readFileSync
as I did it before in this course,
and I'll try to read the data.json file
which doesn't exist here.
There is no data.json file in this project,
so this line of code should fail.
Now, I would get my file data here
as a result of calling readFileSync,
but, again, this will fail.
Now, let's say I thereafter have another step
where I output something totally else,
something like Hi there! as a placeholder,
but, of course, we could be doing
something more useful here.
We could be storing some data in a database
or do anything else that's not directly related
to reading that file.
Now, if we execute readFile here,
that, of course, will fail,
as I mentioned.
If I execute errors.js, we get an error.
Now, this is not an Express application here.
It's just some standard Node code,
some standard JavaScript code
executed with Node using a Node feature.
But, of course, this could be inside
of an Express application.
And if it would be inside of that application,
we could, again, use that error-handling middleware
to send back a default error response.
The problem just is that if we follow this approach,
as soon as this line of code fails here,
this function execution would stop
because if an error occurs,
by default, the function in which that error occurred
will stop executing,
and that error will be passed
to the place where the function was called.
And in Express case,
Express is the thing calling our route handler functions.
And if it receives an error by one of the route handlers,
it therefore then executes
that default error-handling middleware
because it sees there was an error,
and it then executes that middleware.
Here we are calling that function,
and we're not doing anything with that error,
and hence everything fails here.
But that's not the main problem.
The main problem is that even with Express,
we might get that default error handler,
but what if we actually don't want
to stop executing this function
just because this line failed?
Because in programming, it's not always the case
that you wanna throw away everything
and stop executing everything
just because you encounter some error.
You might have a plan B. You might be able to recover.
Or maybe the operation that failed wasn't that important.
Maybe that very next line is super important though.
Here we are just console.logging, but as I said before,
maybe we are writing to a database here.
So that could be a super important operation
which we don't wanna skip
just because this line failed here.
And because you have scenarios like this,
and you have them quite often,
we can also wrap this line here which might fail
with an extra construct
that exists in both browser side and Node.js JavaScript.
We can wrap it with try curly braces.
If we do that,
then JavaScript will try to execute this code here
between those curly braces.
And if that should fail,
we can catch the error that occurred
and handle it in any way we want.
And for this, we add another part to this try statement.
We add the catch keyword
and then another part of curly braces.
And here we now put our code that will run
if the code in the try block failed.
With that, we can now add our own error handling
inside of the function where the error occurred.
So instead of then just escalating it
to the place where we called readFile,
so instead of just passing on the error
to that line of code,
we handle it directly inside this function,
and the code thereafter will execute just fine
because now that error is handled,
and the function will not stop executing
because we're using try-catch.
And if I now save this,
and I execute errors.js,
you see we get an error occurred,
which is that error-handling code we wrote here,
so that's where this message here is coming from.
And then we see Hi there!,
which is this line of code which executes just fine
because the overall function execution of readFile
was not stopped.
And therefore, try-catch is a very useful construct
for handling errors that might occur on your own
with great detail
instead of relying on general error-handling mechanisms.
Those general mechanisms can still be useful,
but often you can also combine them
with fine-grained, custom error handling
where you maybe retry the operation first
before reaching out to a general error-handling mechanism,
or where you maybe create your own object
describing the error
which you then use in your application.
There are a lot of ways of using try-catch,
and you'll see try-catch being used throughout the course,
hence it's important to introduce
and understand it right now.
Now, of course, when you learn about try-catch,
one obvious question that might come up
is why don't we wrap all our code with try-catch?
And there are a couple of reasons for not doing that.
For one, if you wrap everything,
you will also catch a couple of errors
that should indeed crash your application
because they are simply bugs
which you can fix during development,
so which you as a developer should see
whilst you are writing your code.
And if you try and handle them,
you might not spot those errors that quickly.
Another reason for not wrapping everything with try-catch
is that different errors that stem from different sources
often should be handled in different ways.
An error that stems from reading a file
should maybe be handled differently
than an error we might be getting
when we try to get data from a database.
When we fail to read a file,
we might want to retry this after a second or so.
So we might wanna handle this in a different way.
And that's why we don't want
to wrap everything with try-catch,
but only the places where we can expect errors
in some circumstances
and then only as little code as necessary.
That's how you should use try-catch.
But, again, you'll see plenty of examples
of try-catch in action throughout this course.