function fullName ( user ) {
return ` ${ user. firstName } ${ user. lastName } `
}
fullName ( null )
This code will throw an error because null
does not have the properties firstName
and lastName
.
I would even claim that almost everyone would jump to the conclusion that fullName
should not be called with null
but solely with valid user
objects1 .
Real-world applications often aren't as simple as this code snippet.
When applications grow more complex it can be harder to spot the obvious errors as we can do here.
If we extend the example a little bit and add a component that makes use of the fullName
function it already becomes more tricky.
import { fullName } from "./userUtils"
function UserProfile ( { id } ) {
const [ , user] = useFetchUser ( id)
return < > Hello { fullName ( user) } </ >
}
By adding more complexity to our application we need to take a closer look to determine which part isn't behaving in the correct way.
My assumption is that this is what can put less experienced developers off.
The error you see in your browser will still happen inside the fullName
method.
A fix that I've seen a lot in these situations would be to change the code to not throw any more in this particular situation.
function fullName ( user ) {
if ( user == null ) {
return ""
}
return ` ${ user. firstName } ${ user. lastName } `
}
Admittedly, the application won't break anymore.
What's the issue then?
With this solution we haven't fixed the issue, we have hidden the issue.
The problem is still there, it only does not surface at this place anymore.
How can we find the correct spot for the fix?< > >
I would argue that this is where proper reproduction steps help you a lot.
For this particular bug the repro steps could be:
Expected : The user profile tells me it cannot load.
Observed : The page crashes.
Why does this help?
Because it gives us context !
We know where the error surfaces (i.e. in the fullName
method) and when it happens (when there is no internet connection).
When we're debugging the code looking for the problem we can ignore code paths that have either nothing to do with network connections or do not include the fullName
method.
This could reduce the number of possible code paths that might contain the problem a lot.
Do you now spot something when you have another look at the component I showed you earlier?
import { fullName } from "./userUtils"
function UserProfile ( { id } ) {
const [ , user] = useFetchUser ( id)
return < > Hello { fullName ( user) } </ >
}
Did you notice that the useFetchUser
hook returns a tuple and that we're ignoring the first part of it?
Since useFetchUser
sounds a lot like network interaction and we know that has something to do with our problem I would dig deeper into that method.
function useFetchUser ( id ) {
try {
const user = syncFetch ( ` /user/ ${ id} ` )
return [ null , user]
} catch ( error) {
return [ error, null ]
}
}
As it turns out the part of the tuple that our code ignores indicates whether we could fetch the user or not (i.e. whether an error happened while we tried or not).
We've found the actual problem!
What was missing is not a null
check in the fullName
method but proper error handling in the UserProfile
component.
import { fullName } from "./userUtils"
function UserProfile ( { id } ) {
const [ error, user] = useFetchUser ( id)
if ( error) {
return < > Could not load the user! </ >
}
return < > Hello { fullName ( user) } </ >
}
My advice is to always think about how sensible the guards are that you add to your components or methods.
In this example, I would have asked "How much sense does it make that the fullName
method handles null
as an input?".
If there is a logical reason for that then this is totally fine.
Your fix might be in the right place!
But if it does not make too much sense then you might want to take a step back and revisit whether you might be fixing a symptom and not the problem itself.
Tag along the path from what the user sees to where the code broke and ask the same question at every level.
Repeat until you've found the place that contains the code path that breaks and which handles the part of the behavior that you know from the reproduction steps.
The 5 Whys< > >
I almost forgot to include this method because once you get used to doing this it becomes muscle memory.
The 5 whys is a method of literally asking "Why?" five times in a row.
This may sound silly but it helps to get to the bottom of a problem in a lot of cases.
In our example this could look like:
Why does fullName
break? : Because user
is null
.
Why is user
null
? : Because it is passed as a null
reference from UserProfile
.
Why does UserProfile
pass it as a null
reference? : Because network errors aren't handled properly.
In this case, the third why let us to where the problem is.
This method, by the way, does not only work for finding problems in code.