Cannot read property of null or The Art of Assertive Programming
Introduction
Imagine yourself orchestrating a large family meal. Numerous dishes are simmering on the stove and roasting in the oven, each requiring your undivided attention. It’s not just about stirring the pot or monitoring the oven timer, it’s about making sure the soup has the right hint of salt or that the roast isn’t charring into a burnt mess. Your vigilance is needed - you can’t afford to wait until each dish is cooked to confirm it’s done right. Real-time checks are vital, ensuring any errors are caught immediately, not after the fact. In essence, this culinary roller-coaster demands nothing less than your assertive
attention.
The problem
In our journey as programmers, it’s quite common to work with variables that we trust to always be appropriately defined - we assume they come equipped with a preset value, a specific type, a well-structured form, or even particular restrictions, such as being either positive or negative. But what if our expectations aren’t met?
What happens if, instead of receiving an expected value, we get null? What if a string shows up where we expected a number? What becomes of our computations when we encounter zero in our division operations? Or what if we attempt to access array properties from a string, especially when that string also happens to have a property of the same name? Suddenly, we grapple with what we term undefined behavior
, an unpredictable state of our program which seems to diverge all established norms.
Sure, employing a typed language can help alleviate some of these issues, particularly where incorrect types are in play. But how about the other challenges? Realistically, we can’t completely ward off such occurrences since the future remains unpredictable. However, this article isn’t focused on preventing these scenarios, but rather, seeks to address the ideal way to manage them when they arise.
You might be led to think occasional errors are acceptable, given you can always debug the program later. However, what if you had tools to simplify your debugging process? Consider a typical error like Cannot read property 'name' of null
. The origin of this error is usually easy to pinpoint through the stack trace, as with most errors of a similar nature. But what about a scenario where the function completes without throwing an error, albeit yielding an incorrect result? Identifying the source of such a problem could prove more difficult and require more time to debug.
Let’s take a look at some cases to illustrate the problem, to help understand across many languages, the cases are expressed as pseudo-code.
Case 1
Suppose we have a function that accepts a number and returns its power of two.
|
|
If we were to call this function with a string, we would get an error.
Case 2
Now, let’s consider another example. Suppose we have a function that accepts two numbers and returns their sum.
|
|
If we were to call this function with a string and/or number, we would get a successful result as most languages will coerce the number to a string and concatenate the two values.
Case 3
Now, let’s consider another example. Suppose we have a function that accepts a number and returns its square root.
In reality square root of a negative number exists.
|
|
If we were to call this function with a negative number, we would get an error.
The solution
The solution to this predicament lies in actively monitoring and validating your variables and input arguments. When a function receives an input or an argument, you can establish checks to confirm that it meets your criteria. Should it fall short, your program could respond by immediately throwing an error.
This technique is often referred to as an assertion
. An assertion serves as a powerful instrument to bolster the robustness, security, and efficiency of your code. This systematic scrutiny of variables and arguments allows for proactive error detection, making your code not only more resistant to unpredictable behaviour but also simpler to debug and maintain.
What is assertion?
An assertion
is a statement that evaluates to either true or false. It’s a way of expressing a condition that must be true at a particular point in your program. If the condition is false, an error is thrown.
The pseudo-code below illustrates the concept of an assertion.
Examples
|
|
The condition
is the expression that must evaluate to true. If it evaluates to false, the message
is thrown as an error. The message
is optional, but it’s a good practice to include it, as it provides additional context to the error.
Let’s look at a simple example:
|
|
In this example, we generate a random number between 1 and 10 and assign it to the variable a
. We then assert that a
must be greater than 5. If a
is less than or equal to 5, an error is thrown with the message “a must be greater than 5”. This example does not make sense in real program, but it illustrates the concept of an assertion.
Here is a better example:
|
|
In this example, we assert that b
must not be zero. If b
is zero, an error is thrown with the message “b must not be zero”. This assertion prevents the function from dividing by zero, which would otherwise result in another error unhandled error.
And the last example:
|
|
In this example, we assert that a
and b
must be numbers. If either a
or b
is not a number, an error is thrown with the message “a must be a number” or “b must be a number”. This assertion prevents the function from adding non-numbers, which would otherwise execute successfully but yield an incorrect result.
When to use assertion?
Assertions are best used to validate input arguments and variables that come from an external source which are used for internal purposes. For example, the following is a correct use of assertion:
|
|
In this example, we assert that user_id
is actually a non-empty string. We then assert that the user must have an email because the function is not supposed to be called for user without email. These assertions prevent the function from executing with invalid input arguments or variables.
Note: We check user.email only because the function expected to be called only for users with email, if it was expected to be called for users without email, we should not check it.
Note: Also we can check if we suppose that email should be present in user object.
Why did we use assertion for checking user email? Read more about it in the next section.
Should I use assertion for external data grabbing?
Short answer - yes, long answer - it depends. Here is a real example you can better understand it with.
Consider a scenario where you possess an orders
table in your database comprising standard fields, such as order_id
, customer_id
, order_date
, product_id
, and quantity
. You are in need to introduce a new field named status
, an enum accommodating the values pending
, shipped
, or delivered
. You would set this field, by default, as pending
for all new orders. A certain department has assured you that they would assign the status
to the existing orders. Meanwhile, there’s a function that currently runs fine, calling upon each order daily to perform specific operations. Now, your task is to adjust this code to make use of the status for some corresponding logic. Here is that method:
|
|
Essentially, the existing code aligns with the older logic and is likely to function smoothly since it doesn’t utilize the new status field. Post incorporating the new logic, we aim to email customers about all order statuses with the exception of the delivered
ones. However, we could potentially face an issue if the status field isn’t defined. Even though assurances have been received stating this wouldn’t happen, we must prepare for the possibility.
Directly sending a null value to the function isn’t feasible, as it would result in undefined behaviour. Similarly, neglecting it ain’t an option as it would leave the order unprocessed. In this complex situation, the most suitable alternative is to throw an exception and inspect that particular order to understand why it lacks a status.
Indeed, we could insert an if statement to perform the respective action. Yet, if we envision a function with numerous if statements, it could make the code convoluted and challenging to maintain.
In contrast, using assertions can effectively condense each statement to a single code line, as shown below:
|
|
When not to use assertion?
For control flow
Assertions are not meant to be used for control flow. For example, the following is an incorrect use of assertion:
|
|
In this example, we assert that a
and b
must be numbers. We then assert that b
must be negative if a
is negative. This is an incorrect use of assertion because it’s being used for control flow. Instead, we should use a conditional statement:
|
|
In this example, we assert that a
and b
must be numbers. We then check if a
is negative. If a
is negative, we check if b
is negative. If b
is not negative, we throw an error. This is a correct use of assertion because it’s not being used for control flow.
The example provides an explanation of what control flow is within the context of a method, but what are the actual disadvantages of employing assertions for managing control flow?
Drawbacks of using assertions for control flow
Assertions can be disabled globally
- it’s very popular behaviour, so you can’t rely on them with real data.Poor handling of errors
- usually you can’t handle assertion errors properly so you just log them to debug later, and you won’t be able to notify client about the error.Bad user experience
- you can’t validate user input with assertions, because it’s always difficult to make a parsable error message for the user.
For types in statically typed languages
If you use a statically typed language, you don’t need to use assertions for types, because the compiler will catch type errors for you.
For fulfilling non-critical failing
When you have an opportunity to introduce an assertion into a statement, but also want execution to persist even if the assertion fails, you should use a conditional statement instead of an assertion, then you can manage the situation is other way rather than using an assertion.
When does this turn into a solution for us?
When a programmer works on a program alone, he can easily track the state of the program and prevent it from entering into an invalid state. But when multiple programmers work on the same program, it becomes difficult to track the state of the program as the program grows, the functions are called from different places with different arguments, different parts of the program change and the behaviour not always is as expected. This is where assertions come into play. You’re lucky if you have time to write tests for your code, to check edge cases, but even thought it takes a lot of time to cover every edge case.
Assertions slow down the program?
Assertion underhood is just a simple if statement, so it doesn’t slow down the program, even though if you think it’s a problem for you and you expect to be the fastest program in the world, you can disable assertions in production, so many errors will be caught in testing environment.
Assertions replace tests?
Partially yes, but not completely. Assertions are used to check the state of the program, but tests are used to check the behaviour of the program.
At the same time the principle is similar to typings. If you say your function accepts number you don’t have to write tests to check if the argument is a number, that’s the responsibility of the caller to pass a number, not a string, in that case you are not testing the behaviour. The same with assertions, if you say your function accepts a number and you write assert to check if the argument is not zero, you don’t have to write tests to check if the argument is not zero, because you already know the result of that execution, it will throw an error, so you don’t have to test it.
How to handle errors produced by assertions?
You can handle errors produced by assertions the same way you handle errors produced by your code. You can catch them and handle them in a specific way, or you can let them bubble up to the caller.
For a backend application you can just log the error and return a 500 status code, for a frontend application you can show a message to the user and let him know that something went wrong (same applies to a mobile application or any other ui application).
Rules of thumb
Here is the list of rules for using assertions:
- Use assertions to check for things that are very unlikely to happen, but if they do happen, they indicate a bug in your program.
- Use assertions to document assumptions made in the code, making it more readable and maintainable.
- Use assertions to check the state of a program and ensure it is behaving as expected.
- Don’t use assertions for predictable error conditions (like invalid user input or file not found).
- Don’t use assertions to handle or rectify errors. Their purpose is to highlight errors for easier debugging.
Personal opinion
Assertions is easy to use and very powerful tool, it helps to write more readable and maintainable code, it helps to prevent unexpected behaviour of the program. All the recommendations are very reasonable and I agree with them, but if you feel like you want to add an assertion which breaks the rules - just do it, the more you use them the better you understand their usage, it’s always better to add assertion to the place where it should be rather than not to add it at all.
Conclusion
In conclusion, assertive programming
enables us to write robust, secure and efficient code by actively checking and validating state of a program. It’s about setting conditions that must be true at a certain point in our program and throwing an error if they are not. When used properly and responsibly, assertions can enhance the clarity and comprehensibility of our code, reduce debugging and maintenance time, and increase the overall quality of our software.
However, as with most tools, assertive programming has its limits. It’s not a replacement for well-thought-out control flow, validation of user input, or perceptive exception handling in general. Nor is it a replacement for a comprehensive test suite, as assertions and tests have their distinct roles.
Rather than seeing assertions as the panacea for our code quality, we should view them as an additional layer of defense, a form of “active documentation” that declares and enforces our assumptions about how functions are intended to behave. They complement other good coding practices, providing a more assertive and proactive approach to crafting reliable and maintainable software, and should be an important part of any developer’s toolkit.
Just like in the kitchen, “assertive” attention to detail can make all the difference between a successful meal-or a successful application-and a disastrous one. Happy assertive coding!