Using ‘null’ is making your code blind — This is why your code must throw exceptions
I began my coding journey with Java back in 2016. I was primarily into Android projects, and Kotlin wasn’t yet promoted by Google. It was normal for anything to be null
, and late
initialisation was the same as null
.
At present, I mostly work with Kotlin, Dart and TypeScript. All of these languages take null
very seriously — Their use case completely justify this null-aware behaviour.
With time, there is also a visible shift in coding practices. Functions are often seen to return nullable types. Consider this simple function that makes a network request to fetch user data:
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<User?> fetchUser(int userId) async {
final url = 'https://example.com/users/$userId';
try {
final uri = Uri.tryParse(url);
if (uri == null) {
return null;
}
final response = await http.get(uri);
if (response.statusCode == 200) {
final jsonData = json.decode(response.body);
final user = User.fromJson(jsonData);
return user;
} else {
return null;
}
} catch (error) {
return null;
}
}
Nothing seems wrong about this, right? Now let’s try to read the function signature out loud.
The
fetchUser
function takes anint
as a parameter and returns either aUser
object ornull
.
Alright, let’s make it abstract
The function
fetchUser
may or may not return a User.
Well that sounds a bit odd. A function named fetchUser
should certainly return a User
. So when may it not return one? Of course when something unexpected happens. In this example, one of these things:
- The
Uri
parsing might fail. - There might be a problem establishing network connection.
- The server may give an error response.
- The JSON data to User object conversion might fail.
- The number of edge cases increase with functional complexity.
But these are not ideal scenarios and are thus, exceptional cases — why not call them ✨ Exceptions! ✨
To contrast, this is what an ideal version of the same function would look like:
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<User> fetchUser(int userId) async {
final url = 'https://example.com/users/$userId';
try {
final uri = Uri.tryParse(url);
if (uri == null) {
throw FormatException('Failed to parse URL');
}
final response = await http.get(uri);
if (response.statusCode == 200) {
final jsonData = json.decode(response.body);
final user = User.fromJson(jsonData);
return user;
} else {
throw http.ClientException('Failed to fetch user: ${response.statusCode}');
}
} on FormatException {
// only throws [FormatException]
rethrow;
} on http.ClientException {
// only throws [http.ClientException]
rethrow;
} catch (error) {
// throws unexpected [Exception]
throw Exception('Something went wrong: $error');
}
}
This function makes it absolutely clear that its intent is to fetch a User
based on a given userId
. Anything happening otherwise is an exception and not an accepted or expected outcome.
A function signature should only represent its expected behaviour and must not account for exceptional cases.
By declaring User?
instead of User
we state that getting null
is one of the expected outcomes — not fair!
But why does it matter?
The biggest downside of returning a nullable value instead of throwing exceptions is that the caller of the function will never get to know what exactly went wrong. Using the first version of our fetchUser
function the caller will have little to no knowledge of the function result. When things go wrong in production, you will end up displaying a featureless error message like “Something went wrong”. This impacts user interest and is not a good experience.
On the other hand, using the second version of the fetchUser
function the caller can catch those exceptions, get error messages, and decide what error to display. Of course, you may add as many throw
blocks as you wish to have precise control.
So where should we use null?
In variables — both local and global — to keep track of its initialisation. Null is good only if we do not misuse it. You may say we can just use the late
modifier, but I will share in a subsequent blog why late
might be a slow poison for your Dart/Flutter project.
Conclusion
Using null
may look like an easy bail out when writing code, but it takes away a good amount of control. Remember that you write a function once and reuse it multiple times — don’t do anything that would make you rewrite the function multiple times.
There may be scenarios where returning a nullable may seem justified, but there aren’t many. Let me know how and where you use null and let’s meet in another blog. Happy coding!