Welcome to module 2 about
panicking.
In this module, we'll define what panic
means in Rust
and then we'll look at how to use the panic! macro
to cause your program to panic.
We'll discuss situations in which you
should cause your program to panic
and look at some examples.
And we'll round out this module by looking
at some other macros that ultimately call
the panic! macro
but provide different semantics for some other
situations.
So, what is panicking?
Imagine a thread of your program
as the machine in a factory
chugging along, happily processing
the data that it's given.
Then, it comes across a value
that doesn't make any sense.
Processing that value in the same way
as the values that make sense
would result in garbage at best
and would open up the system to vulnerabilities
at worse.
Rather than continuing normal processing
with an invalid input,
panicking is like an emergency stop
button.
Pushing the emergency stop button
causes the thread to stop what it's doing
immediately instead.
This prevents the program from continuing
with garbage that might be incorrect
or dangerous.
Here's an example in a program.
Say we're writing a command-line program
that can process data for Windows,
Linux, or macOS.
The user of the program will specify
the operating system they want to process data
for by using the -p
flag for platform.
What should the program do
if the user enters a value like oranges
that isn't one of the operating systems it
knows how to process data for?
Picking an arbitrary operating system
and continuing would be incorrect.
The user clearly meant to specify something
but we're not likely to guess what they wanted correctly.
The right thing to do in this case is to
stop the thread
with the message to the user that the
program can't do anything
with this invalid value,
and they need to start the program again
with a different value.
Let's see how to write this in code
with the panic! macro.
You can call the panic! macro at any point
in your code.
In this first example, we're calling
panic! as the only action taken in
main.
If we don't specify any arguments,
when we run this code, we'll see the message,
thread 'main' panicked at 'explicit panic',
and the location in our code where the panic
came from.
If we specify a message as an argument
to panic!,
then that message appears in the output
instead of 'explicit panic'.
You can also give panic! the same kinds
of arguments you can give to println,
a format string
with placeholders
and variables that go
with those placeholders.
This lets you explain better what you got
that wasn't expected.
This panic call prints
'Something's not right here!
Got value: no' as its message.
Let's put into code
the logic we talked about for the command-line
application that can produce
different output for different operating
systems.
Here, we've defined an enum
for the platforms
our program supports.
We have a parse function, defined
as a function associated
with the Platform enum
that takes an argument named platform_arg.
platform_arg represents
the value you would get from the command-line argument.
We've excluded the actual command-line argument
parsing to simplify this example.
The parse function checks to see if
the platform_arg value
is any of the values we expect
and, if so, returns
the corresponding enum variant.
In the else case, we call the
panic! macro
with the value that we didn't recognize
and stop the program.
In main, we call the parse function
and then we have a println that uses
the returned platform value.
If we run this example
with the string Windows for platform_arg,
that value is
recognised and we get to the final print
line.
If we change the platform_arg value
to something that we don't recognize
as an operating system,
when we run it, the program stops with the
panic message
and never gets to the final print line.
Let's talk some more about when it's good to panic
and look at a few more examples.
If you're trying to decide whether
or not to panic, consider whether
taking the next action would ever be
valid given the situation you're in.
For example, there is no default
all users would find reasonable.
The command-line argument example fits
into this case.
Another example is out-of-bounds
memory access, which we discussed
in the Slices module in unit 2.
If we have, say, a vector of three
items
and we try to access an item
in index 50,
when we run this, Rust will panic
and say the index is out of bounds.
In this case, panicking is important
to prevent a security vulnerability
called a buffer overread.
C will happily return to you memory
that doesn't belong to the current data structure
and this has been an entry point of lots
of exploits.
Another aspect to consider is that,
by panicking, you're making the decision
for any code calling this function that
there's no way for it to try to recover
either. We're going to talk
about recovering from errors in the coming modules.
Another time to panic is when the problem
must be fixed by a programmer
changing buggy code, not
a user changing their input.
Let's look at an example from the HTTP
crate.
This is the documentation for the header
name from_static function,
one of the ways to create a HeaderName instance.
This function takes a string literal,
which you'll recall from the Slices
module in unit 2 is a string
that literally appears in your source code.
If the string literal provided
isn't a valid HeaderName, such
as a string containing special characters,
this function will panic to alert you
that your hardcoded string is incorrect.
The only way to fix this panic is
to edit the string literal
in the code and recompile.
There's no way an end user could fix
this.
A time when you shouldn't panic
is when failure is the normal occurrence
given the operation you're doing.
For example, web browsers
might receive malformed HTML.
If your browser panicked
and shut down every time you visited
a site that didn't have valid HTML,
you'd probably get frustrated
and use a different browser.
Instead, your browser takes various
efforts to recover
and present what it can in the face
of invalid HTML.
Despite these guidelines,
you can always choose to panic before you've
decided how to handle errors,
then you can search for calls to panic
to update your error handling code later.
Finally,
let's talk about a few other macros
provided by the Standard Library
that also cause a panic!.
First is the unreachable! macro.
This is useful for indicating to the compiler
that it's impossible to get to a certain point
in the code.
This is useful when we're required to cover
all cases that the compiler thinks are
possible,
but because of some other constraint,
we as humans know certain cases aren't
possible.
For example, here's a program
with an enum for the state of a door,
which can be Opened
or Closed.
We also have an enum for the actions that
can be taken on the door, which
are Open and Close.
In a function that takes the current state
of a door and an action to take
on the door,
we need to handle how to close an open
door
and how to open a closed door.
The other two states, opening an open
door and closing a closed door,
shouldn't ever happen.
Rust requires a match expression
to cover all cases, so
we can use the underscore to catch the impossible
cases and call it the unreachable! macro.
If we're wrong
and somehow the program does get to
this spot in the code, such
as with this main function where we're calling
the function with the unsupported values,
the unreachable! macro will call the panic!
macro
with the message, 'internal error:
entered unreachable code'.
It's preferable to use unreachable!
rather than a panic! in this situation
because it conveys our intent to future
readers of this code.
The next macro, unimplemented!,
is useful when starting work on a new feature.
This macro communicates that we intend
to write code that goes in its place
but we haven't yet.
In our previous example, where we had comments
showing where we would add code to handle
door actions, we could instead
have put calls to the unimplemented! macro.
If a program gets to an unimplemented case,
it will panic with the message, 'not yet
implemented'.
The assert family of macros will panic
if a given condition isn't true.
assert! takes a Boolean
value and panics if it is false.
assert_eq! takes two values
and panics if they aren't equivalent,
and assert_ne! takes two
values and panics if they are equivalent.
These macros are commonly used in
two situations.
The first situation is to check preconditions
at the beginning of a function.
In the rest of the function,
you can proceed knowing these conditions
are true and not have to check them in multiple
locations because the program
would have stopped earlier if they weren't true.
The second, much more common, situation
the assert macros are used in, is in
tests.
The way test functions work in Rust
is that if the code inside a test function
panics, that test fails.
The assert macros make it convenient to write
clear tests that compare the values
you expect with the values
the code under test produces.
In this module,
we discussed that panicking is stopping the program
if it's in an invalid state
and that we do this by calling the panic! macro
with an optional message.
We discussed how it's a good idea to panic
when continuing would be incorrect,
there is no way for the calling code to recover,
the problem must be fixed by changing the
code rather than inputs,
when failure isn't expected to happen regularly,
and when we haven't decided how we want
to handle errors.
We talked about other macros that panic,
including unreachable!,
unimplemented!, and
the assert macros.
In the next module,
we'll talk about recoverable errors
and how to handle them using the types Result
and Option.