1
00:00:00,270 --> 00:00:01,740
Welcome to module 5
2
00:00:01,990 --> 00:00:04,169
on the advantages of Rust's error handling
3
00:00:04,369 --> 00:00:04,770
strategy.
4
00:00:06,300 --> 00:00:08,369
Now that we've covered how to handle errors
5
00:00:08,569 --> 00:00:10,349
in Rust, in this module,
6
00:00:10,549 --> 00:00:12,299
we'll editorialize a bit about
7
00:00:12,499 --> 00:00:14,369
the benefits that we've seen from separating
8
00:00:14,569 --> 00:00:16,220
bugs from recoverable errors.
9
00:00:17,870 --> 00:00:18,620
With bugs,
10
00:00:18,830 --> 00:00:20,670
the advantages include failing loudly, fast, and
11
00:00:20,870 --> 00:00:22,370
close to the cause,
12
00:00:22,920 --> 00:00:24,799
and documenting and enforcing assumptions
13
00:00:24,999 --> 00:00:25,790
that the code has made
14
00:00:25,990 --> 00:00:27,910
and needs to be true in order to work.
15
00:00:29,520 --> 00:00:31,100
For recoverable errors,
16
00:00:31,620 --> 00:00:34,169
the advantages include knowing about the possibility
17
00:00:34,369 --> 00:00:35,940
of errors from the type system,
18
00:00:36,470 --> 00:00:38,579
and the compiler making sure you don't forget
19
00:00:38,779 --> 00:00:39,690
to handle errors.
20
00:00:41,340 --> 00:00:43,649
Then, because all engineering choices
21
00:00:43,849 --> 00:00:44,460
come with trade-offs,
22
00:00:45,270 --> 00:00:46,220
we'll end the module
23
00:00:46,420 --> 00:00:48,030
by talking about the disadvantages
24
00:00:48,230 --> 00:00:49,620
of Rust's error handling strategy,
25
00:00:49,980 --> 00:00:51,660
and how it affects your development flow,
26
00:00:51,860 --> 00:00:53,699
and how it can be tricky when there are multiple
27
00:00:53,899 --> 00:00:54,950
error types that can happen.
28
00:00:56,490 --> 00:00:58,619
Let's start by recapping the difference
29
00:00:58,819 --> 00:00:59,520
between bugs
30
00:00:59,720 --> 00:01:00,860
and recoverable errors.
31
00:01:02,360 --> 00:01:03,640
In previous modules,
32
00:01:03,840 --> 00:01:05,540
we talked about panic! and Result
33
00:01:06,050 --> 00:01:07,879
and we talked about situations in which to
34
00:01:08,079 --> 00:01:08,600
use each.
35
00:01:09,110 --> 00:01:11,750
To review, panics are for situations
36
00:01:11,950 --> 00:01:13,700
that indicate we're in a bad state,
37
00:01:14,000 --> 00:01:15,650
from which there's no way to recover,
38
00:01:16,100 --> 00:01:17,929
or a bug that would need to be fixed by
39
00:01:18,129 --> 00:01:18,570
a programmer.
40
00:01:19,600 --> 00:01:21,410
Results are for situations
41
00:01:21,610 --> 00:01:23,230
we expect to happen sometimes,
42
00:01:23,480 --> 00:01:25,449
that the program may be able to take some
43
00:01:25,649 --> 00:01:26,710
action to recover from,
44
00:01:27,190 --> 00:01:29,109
possibly with the help from the end user
45
00:01:29,309 --> 00:01:31,209
of the program, as opposed to needing
46
00:01:31,409 --> 00:01:32,710
to change the code of the program.
47
00:01:34,330 --> 00:01:36,070
Many other programming languages
48
00:01:36,270 --> 00:01:38,109
use the same mechanism for handling
49
00:01:38,309 --> 00:01:40,060
both bugs and recoverable errors,
50
00:01:40,540 --> 00:01:42,460
but they're actually separate situations
51
00:01:42,660 --> 00:01:44,380
that deserve separate handling.
52
00:01:44,900 --> 00:01:46,840
Rust builds appropriate handling
53
00:01:47,040 --> 00:01:48,190
into panic and Result
54
00:01:48,630 --> 00:01:51,190
if you use the one that best fits the situation.
55
00:01:52,680 --> 00:01:54,509
So, let's talk about the advantages you
56
00:01:54,709 --> 00:01:56,400
get from Rust's handling of bugs,
57
00:01:56,910 --> 00:01:58,859
starting with how panics cause your program
58
00:01:59,059 --> 00:02:00,570
to fail loudly, fast,
59
00:02:00,770 --> 00:02:01,890
and close to the cause.
60
00:02:03,380 --> 00:02:05,430
We're going to compare a Rust program
61
00:02:05,630 --> 00:02:07,649
and a C program that both have the same
62
00:02:07,849 --> 00:02:09,630
bug reading past the end
63
00:02:09,830 --> 00:02:10,619
of a data structure.
64
00:02:11,160 --> 00:02:12,989
This type of problem is known as a
65
00:02:13,189 --> 00:02:14,130
buffer overread.
66
00:02:15,690 --> 00:02:17,790
These two programs both initialize
67
00:02:17,990 --> 00:02:19,950
a collection of integers named accts,
68
00:02:20,150 --> 00:02:21,210
containing two values,
69
00:02:21,780 --> 00:02:23,670
and then print the value at position
70
00:02:23,870 --> 00:02:25,619
11 in the collection, which is out
71
00:02:25,819 --> 00:02:26,220
of bounds.
72
00:02:26,760 --> 00:02:28,740
Now we're going to illustrate what we mean
73
00:02:28,940 --> 00:02:30,450
by failing loudly, fast,
74
00:02:30,650 --> 00:02:31,650
and close to the cause.
75
00:02:33,110 --> 00:02:34,300
By failed loudly,
76
00:02:34,690 --> 00:02:36,410
we mean that when your program encounters a
77
00:02:36,610 --> 00:02:38,529
problem, it lets you know
78
00:02:38,729 --> 00:02:39,940
in a really obvious way,
79
00:02:40,390 --> 00:02:42,040
as opposed to failing silently
80
00:02:42,240 --> 00:02:44,050
and continuing on without any way
81
00:02:44,250 --> 00:02:45,580
for you to tell that something is wrong.
82
00:02:47,120 --> 00:02:49,129
The Rust program compiles without any
83
00:02:49,329 --> 00:02:49,780
errors,
84
00:02:50,030 --> 00:02:52,029
but when we run it, the program panics
85
00:02:52,229 --> 00:02:54,260
with the message index out of bounds -
86
00:02:54,740 --> 00:02:56,560
that's a loud, obvious failure.
87
00:02:58,060 --> 00:03:00,070
The C program also compiles,
88
00:03:00,430 --> 00:03:02,540
but when we run it, there's no error,
89
00:03:02,800 --> 00:03:05,409
it just prints an unexpected
90
00:03:05,609 --> 00:03:06,730
and incorrect result.
91
00:03:07,210 --> 00:03:08,150
I get 0 -
92
00:03:08,470 --> 00:03:09,700
you might get something different.
93
00:03:10,480 --> 00:03:12,060
The program has silently continued
94
00:03:12,260 --> 00:03:12,950
with bad data.
95
00:03:14,410 --> 00:03:16,360
The advantage is that loud failures
96
00:03:16,560 --> 00:03:18,440
are more likely to be noticed during the development
97
00:03:18,640 --> 00:03:20,490
or testing of a feature, rather
98
00:03:20,690 --> 00:03:22,449
than getting out to production where such
99
00:03:22,649 --> 00:03:24,070
failures affect end users.
100
00:03:25,540 --> 00:03:27,369
This example also illustrates
101
00:03:27,569 --> 00:03:29,110
what we mean by fail fast.
102
00:03:29,560 --> 00:03:31,449
As soon as the Rust program was asked
103
00:03:31,649 --> 00:03:33,610
to perform an invalid operation,
104
00:03:33,820 --> 00:03:34,420
it stopped.
105
00:03:35,360 --> 00:03:37,400
The C program continued on without
106
00:03:37,600 --> 00:03:38,200
ever stopping.
107
00:03:38,840 --> 00:03:41,270
We could have performed all sorts of operations
108
00:03:41,470 --> 00:03:42,440
on the invalid data.
109
00:03:43,930 --> 00:03:44,670
Exploits
110
00:03:44,870 --> 00:03:46,790
take advantage of our program continuing
111
00:03:46,990 --> 00:03:48,729
with bad data to get programs
112
00:03:48,929 --> 00:03:50,830
to do something they're not supposed to do.
113
00:03:51,030 --> 00:03:52,780
The pervasive usage of panics
114
00:03:52,980 --> 00:03:54,160
in Rust's Standard Library
115
00:03:54,360 --> 00:03:56,290
and the ecosystem don't give hackers
116
00:03:56,490 --> 00:03:57,280
that opportunity.
117
00:03:58,840 --> 00:04:00,849
Related to failing fast is failing
118
00:04:01,049 --> 00:04:02,799
close to the source.
119
00:04:02,999 --> 00:04:04,390
When debugging, it takes longer
120
00:04:04,590 --> 00:04:06,250
if you have to trace through lots of code
121
00:04:06,550 --> 00:04:08,410
to figure out where an unexpected value
122
00:04:08,610 --> 00:04:08,980
came from.
123
00:04:10,500 --> 00:04:12,369
While you don't typically want your Rust
124
00:04:12,569 --> 00:04:14,219
programs to panic, at least
125
00:04:14,419 --> 00:04:14,870
when they do,
126
00:04:15,130 --> 00:04:17,110
it'll be easier to figure out why
127
00:04:17,310 --> 00:04:19,028
because the panic! happened right where
128
00:04:19,228 --> 00:04:19,910
the problem occurred.
129
00:04:21,430 --> 00:04:23,289
Next, let's look at how using
130
00:04:23,489 --> 00:04:25,359
panic! to enforce expectations
131
00:04:25,559 --> 00:04:26,440
can catch bugs.
132
00:04:27,960 --> 00:04:30,090
There is a method on Result and Option
133
00:04:30,290 --> 00:04:32,279
called expect that has similar
134
00:04:32,479 --> 00:04:34,220
behavior to this match expression
135
00:04:34,420 --> 00:04:36,209
we used in the module on Result and
136
00:04:36,409 --> 00:04:36,750
Option.
137
00:04:37,230 --> 00:04:38,880
If the value is the Ok
138
00:04:39,080 --> 00:04:41,120
or Some variant, it returns
139
00:04:41,320 --> 00:04:42,010
the inner value.
140
00:04:43,010 --> 00:04:44,600
If the value is the Err
141
00:04:44,800 --> 00:04:46,630
or None variant, it panics
142
00:04:46,830 --> 00:04:48,619
with the message given as an argument to
143
00:04:48,819 --> 00:04:49,190
expect.
144
00:04:50,650 --> 00:04:52,700
This is useful when you have a Result or
145
00:04:52,900 --> 00:04:54,539
Option, and you're mostly
146
00:04:54,739 --> 00:04:56,370
sure the operation will succeed
147
00:04:56,570 --> 00:04:57,640
or produce a value.
148
00:04:57,960 --> 00:05:00,009
The expect documents that assumption
149
00:05:00,209 --> 00:05:00,650
of yours,
150
00:05:01,040 --> 00:05:02,960
and if you're ever wrong, you'll get a loud
151
00:05:03,160 --> 00:05:03,450
failure.
152
00:05:04,960 --> 00:05:06,850
Now, let's look at the advantages you get
153
00:05:07,050 --> 00:05:09,110
from using Result for recoverable errors,
154
00:05:09,640 --> 00:05:11,529
starting with how it makes the possibility
155
00:05:11,729 --> 00:05:12,460
of errors clear.
156
00:05:13,960 --> 00:05:15,879
When a function such as serde_json::from_str
157
00:05:16,079 --> 00:05:17,770
returns a Result,
158
00:05:18,250 --> 00:05:20,199
we know right away, from looking at the signature
159
00:05:20,399 --> 00:05:22,029
in the docs, that the operation
160
00:05:22,229 --> 00:05:22,869
this function performs
161
00:05:23,069 --> 00:05:24,030
might fail.
162
00:05:25,510 --> 00:05:27,310
If we write a function that calls
163
00:05:27,510 --> 00:05:28,740
serde_json::from_str.
164
00:05:29,110 --> 00:05:31,599
Our function needs to either handle the possibility
165
00:05:31,799 --> 00:05:32,230
of failure
166
00:05:32,530 --> 00:05:33,840
or also return Result.
167
00:05:34,300 --> 00:05:36,189
Thus communicating to the code that calls our
168
00:05:36,389 --> 00:05:38,320
function that there is a possibility of failure
169
00:05:38,680 --> 00:05:40,810
even without looking at the implementation.
170
00:05:42,320 --> 00:05:43,250
In contrast,
171
00:05:43,520 --> 00:05:45,290
here's the Ruby documentation for JSON.parse,
172
00:05:45,670 --> 00:05:47,539
which serves the same
173
00:05:47,739 --> 00:05:49,500
purpose as serde_json::from_str.
174
00:05:50,410 --> 00:05:52,279
There's no indication of failure in the
175
00:05:52,479 --> 00:05:53,690
parse function's signature.
176
00:05:55,190 --> 00:05:57,069
If we write a function that calls
177
00:05:57,269 --> 00:05:59,239
JSON.parse, our function signature
178
00:05:59,439 --> 00:06:01,190
doesn't convey that information either.
179
00:06:01,670 --> 00:06:03,890
It's hard to tell what kinds of exceptions
180
00:06:04,090 --> 00:06:05,779
a function in Ruby might raise that we
181
00:06:05,979 --> 00:06:06,790
might want to handle.
182
00:06:08,250 --> 00:06:10,199
Beyond being able to easily see
183
00:06:10,399 --> 00:06:11,850
there's a possibility of failure,
184
00:06:12,270 --> 00:06:14,129
the Rust compiler also makes sure
185
00:06:14,329 --> 00:06:16,140
that we handle that possibility somehow.
186
00:06:17,620 --> 00:06:19,740
As we saw in the module on Result
187
00:06:19,940 --> 00:06:21,639
and Option, we can't use
188
00:06:21,839 --> 00:06:23,619
the inner value from a Result without
189
00:06:23,819 --> 00:06:25,449
doing something explicit to handle the
190
00:06:25,649 --> 00:06:26,200
possibility of errors.
191
00:06:27,820 --> 00:06:29,799
Even if we call expect to turn
192
00:06:29,999 --> 00:06:31,150
an Err into a panic!,
193
00:06:31,570 --> 00:06:33,460
when we decide to implement better error
194
00:06:33,660 --> 00:06:35,650
handling, we can search for expect
195
00:06:35,850 --> 00:06:37,870
and find all the places we need to update.
196
00:06:39,390 --> 00:06:41,820
In contrast are the error handling conventions
197
00:06:42,020 --> 00:06:42,300
in Go.
198
00:06:42,810 --> 00:06:44,610
Functions that might fail return
199
00:06:44,810 --> 00:06:45,900
both a success value
200
00:06:46,100 --> 00:06:47,999
and an error value, rather
201
00:06:48,199 --> 00:06:49,830
than one or the other as Rust does.
202
00:06:50,790 --> 00:06:52,679
The convention is to check whether the
203
00:06:52,879 --> 00:06:54,809
error value is nil right after a
204
00:06:55,009 --> 00:06:56,639
function call, so close to
205
00:06:56,839 --> 00:06:58,499
the source of the problem, which is
206
00:06:58,699 --> 00:06:58,740
good.
207
00:07:00,220 --> 00:07:02,080
The trouble is that the Go compiler
208
00:07:02,280 --> 00:07:03,330
doesn't help you remember to
209
00:07:03,530 --> 00:07:04,270
do this check
210
00:07:04,470 --> 00:07:05,780
or help you check correctly.
211
00:07:06,750 --> 00:07:08,699
It's possible to ignore failure by
212
00:07:08,899 --> 00:07:10,980
assigning the error value to _
213
00:07:11,180 --> 00:07:12,830
and only using the success value.
214
00:07:13,810 --> 00:07:15,850
It's hard to search for underscores later
215
00:07:16,050 --> 00:07:17,580
to add better error handling.
216
00:07:19,090 --> 00:07:20,949
It's also possible to forget to
217
00:07:21,149 --> 00:07:22,750
assign the err to a variable
218
00:07:23,140 --> 00:07:25,330
and then check an uninitialized variable
219
00:07:25,530 --> 00:07:27,429
instead, which makes it look
220
00:07:27,629 --> 00:07:29,349
like you're checking for errors even though
221
00:07:29,549 --> 00:07:31,299
you're not. This problem has
222
00:07:31,499 --> 00:07:32,340
occurred in real code.
223
00:07:32,950 --> 00:07:34,809
In Rust, the compiler helps you
224
00:07:35,009 --> 00:07:36,400
avoid these kinds of problems.
225
00:07:37,910 --> 00:07:39,739
Now that we've covered the advantages
226
00:07:39,939 --> 00:07:41,510
of Rust's error handling strategy,
227
00:07:41,960 --> 00:07:43,879
we'd be remiss if we didn't also
228
00:07:44,079 --> 00:07:46,069
acknowledge the disadvantages, starting
229
00:07:46,269 --> 00:07:47,180
with your development flow.
230
00:07:48,690 --> 00:07:50,519
When you're not quite sure what you're building
231
00:07:50,719 --> 00:07:50,840
yet
232
00:07:51,360 --> 00:07:53,369
or what all the operations a function
233
00:07:53,569 --> 00:07:53,940
needs to do are,
234
00:07:54,570 --> 00:07:56,400
it can be annoying to be forced to think about
235
00:07:56,600 --> 00:07:58,770
which operations might fail in which ways
236
00:07:58,980 --> 00:08:00,150
and what should be done about that.
237
00:08:01,660 --> 00:08:03,549
As a former Rubyist, it's a lot
238
00:08:03,749 --> 00:08:05,379
smoother to just start writing code
239
00:08:05,579 --> 00:08:07,299
in Ruby and get some functionality up
240
00:08:07,499 --> 00:08:07,810
and running.
241
00:08:08,590 --> 00:08:10,539
It's nice to go through the entire happy path
242
00:08:10,739 --> 00:08:12,549
of an application feature without being
243
00:08:12,749 --> 00:08:14,440
slowed down by thinking about errors.
244
00:08:15,970 --> 00:08:17,859
I will say, though, that, in Ruby, I've
245
00:08:18,059 --> 00:08:19,660
gotten more errors in production,
246
00:08:19,980 --> 00:08:21,850
forgotten about more edge cases,
247
00:08:22,090 --> 00:08:23,979
and in some cases never figured out why
248
00:08:24,179 --> 00:08:25,760
my code wasn't behaving correctly
249
00:08:26,020 --> 00:08:27,850
because it's hard to see when you're not
250
00:08:28,050 --> 00:08:29,050
handling failures that happen.
251
00:08:30,540 --> 00:08:31,649
When I'm starting a program
252
00:08:31,849 --> 00:08:34,050
in Rust, I make liberal use of expect
253
00:08:34,250 --> 00:08:36,329
to defer thinking about errors immediately,
254
00:08:36,780 --> 00:08:38,639
but I found that knowing where errors might
255
00:08:38,839 --> 00:08:40,649
happen, and being encouraged to handle errors
256
00:08:40,849 --> 00:08:42,479
correctly, has made my code work
257
00:08:42,679 --> 00:08:43,450
better in production.
258
00:08:45,010 --> 00:08:46,839
Finally, let's talk about functions
259
00:08:47,039 --> 00:08:48,730
that perform operations that result
260
00:08:48,930 --> 00:08:50,170
in multiple error types.
261
00:08:51,650 --> 00:08:53,479
For example, here's a function that
262
00:08:53,679 --> 00:08:55,040
reads an environment variable,
263
00:08:55,310 --> 00:08:57,349
then parses the environment variable string
264
00:08:57,549 --> 00:08:58,700
value into a number
265
00:08:58,900 --> 00:08:59,820
and adds 1 to it.
266
00:09:01,310 --> 00:09:03,350
The function, env::var, returns
267
00:09:03,550 --> 00:09:05,360
a Result with an error type of
268
00:09:05,560 --> 00:09:07,630
env::VarError. The environment variable
269
00:09:07,830 --> 00:09:08,660
might not be present
270
00:09:08,870 --> 00:09:10,550
or it might not be valid Unicode.
271
00:09:11,660 --> 00:09:13,499
Parsing might not work if the environment
272
00:09:13,699 --> 00:09:15,360
variable's value isn't a number.
273
00:09:15,810 --> 00:09:17,730
In that case, the parse function returns
274
00:09:17,930 --> 00:09:19,230
a num::ParseIntError.
275
00:09:20,780 --> 00:09:22,739
What do we put as our function's error
276
00:09:22,939 --> 00:09:23,190
type?
277
00:09:23,780 --> 00:09:25,910
And what do we do if we add more functionality
278
00:09:26,110 --> 00:09:27,320
with different error types.
279
00:09:28,280 --> 00:09:29,480
This is a bit tricky.
280
00:09:29,870 --> 00:09:31,849
The next module is going to explore different
281
00:09:32,049 --> 00:09:33,370
ways to handle this situation.
282
00:09:34,880 --> 00:09:35,810
In this module,
283
00:09:36,170 --> 00:09:38,150
we discussed how Rust treats bugs
284
00:09:38,350 --> 00:09:39,890
and recoverable errors differently.
285
00:09:40,400 --> 00:09:42,040
The way Rust handles bugs
286
00:09:42,260 --> 00:09:44,409
helps you find bugs sooner in the development
287
00:09:44,609 --> 00:09:44,930
cycle
288
00:09:45,290 --> 00:09:47,209
and leaves fewer opportunities for
289
00:09:47,409 --> 00:09:48,350
exploiting your program.
290
00:09:48,860 --> 00:09:51,020
The way Rust handles recoverable errors
291
00:09:51,290 --> 00:09:53,780
helps you to remember to handle all situations
292
00:09:53,980 --> 00:09:55,790
and makes for a more reliable program.
293
00:09:56,330 --> 00:09:58,249
This comes at a cost of added
294
00:09:58,449 --> 00:09:59,770
friction when starting development
295
00:10:00,080 --> 00:10:02,089
because Rust forces you to think about more
296
00:10:02,289 --> 00:10:02,900
than the happy path.
297
00:10:03,410 --> 00:10:05,210
Also, functions that can fail
298
00:10:05,410 --> 00:10:07,100
in multiple ways are tricky to write,
299
00:10:07,580 --> 00:10:09,100
which is what the next module is about.