Using ‘null’ is making your code blind — This is why your code must throw exceptions

Chahat Gupta
4 min readJul 1, 2023

--

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 an int as a parameter and returns either a User object or null.

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:

  1. The Uri parsing might fail.
  2. There might be a problem establishing network connection.
  3. The server may give an error response.
  4. The JSON data to User object conversion might fail.
  5. 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!

--

--

Chahat Gupta

Mobile Tech Lead specialising in Android, iOS, and Flutter. Sharing insights and learnings on mobile development to inspire and elevate tech professionals.