Into the void of silence, into the empty space of nothing, the joy of life is unfurled.
-- C. S. Lewis
Usually, a developer will focus on errors and exceptions to increase the stability of its application. It can be with defensive programming patterns, or like for Erlang, embracing errors and accepts them with the Let it crash philosophy (see also crash-only systems). In Dart... it was weird to see more information about null than errors management. In fact, one of the longest article on dart.dev seems to be dedicated to null safety.
How to deal with errors, exceptions and... null, in Dart? It will probably be the thing I will hate the most with Dart. Dealing with null seems to be really complex, at least, when no external packages are used.
What is null anyway?
The reserved word
nullevaluates to the null object.⟨nullLiteral⟩ ::= nullThe
nullobject is the sole instance of the built-in classNull. Attempting to instantiateNullcauses a compile-time error. It is a compile-time error for a
class to extend, mix in or implement Null. TheNullclass extends theObjectclass and declares no methods except those also declared byObject. Thenullobject has primitive equality (10.2.3). The static type ofnullis theNulltype
-- Dart Specification, section 17.4, page 103
So, null is an unique object only instantiated once during - probably - the initialization.
The treatment of
nullmerits some discussion. Considernull + 2. This expression always causes an error. We could have chosen not to treat it as a constant expression (and in general, not to allow null as a subexpression of numeric or boolean constant expressions). There are two arguments for including it: First, it is constant so we can evaluate it at compile time. Second, it seems more useful to give the error stemming from the evaluation explicitly.
-- Dart Specification, page 101
Errors generate by nulls can be evaluated at compile time, this is helpful for static analysis.
Bootstrapping
For this article, a new project called n will be created. It will be used to play with null in Dart.
$ dart create n
Creating n using template console...
.gitignore
analysis_options.yaml
CHANGELOG.md
pubspec.yaml
README.md
bin/n.dart
lib/n.dart
test/n_test.dart
Running pub get... 2.7s
Resolving dependencies...
Downloading packages...
Changed 48 dependencies!
Created project n in n! In order to get started, run the following commands:
cd n
dart run
$ cd n
Dissecting Null
The Null object got only two attributes, hashCode and runtimeType. The hashCode returns always 0 (zero) and the runtimeType returns the Type of the object, Null in this case. Only two methods are also public, noSuchMethod() and toString().
void main() {
print(null.hashCode);
print(null.runtimeType);
}
0
Null
Let have a quick look on the source code.
@pragma("vm:entry-point")
final class Null {
factory Null._uninstantiable() {
throw UnsupportedError('class Null cannot be instantiated');
}
external int get hashCode;
String toString() => "null";
}
The Null class is implemented in sdk/lib/core/null.dart as a final class. It uses a vm:entry-point pragma, confirming its presence during initialization. the _uninstantiable constructor is a bit odd to me. I don't know if it's convention or something specified somewhere, but because a factory is set, I would assume it is a way to protect against creating new instance during runtime. It could be nice to find some ways to call this method.
Operators
Let have a quick look on the Null operators. The first one to check will be the ! operator. It is only used if you are sure a variable will never be null, if the data is null, then it will raise an exception Null check operator used on a null value.
void t1(dynamic? v) {
var v1 = v!;
print("t1: ${v1}");
}
void main() {
t1(1);
t1(null);
}
$ dart run
Building package executable...
Built n:n.
t1: 1
Unhandled exception:
Null check operator used on a null value
#0 t1 (file:///home/user/tmp/dart/n/bin/n.dart:9:13)
#1 main (file:///home/user/tmp/dart/n/bin/n.dart:5:3)
#2 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:313:19)
#3 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12)
What to do if the value is null but a default value can be used instead? In this situation, the ?? (if-null) operator can be used.
An if-null expression evaluates an expression and if the result is the null object (17.4), evaluates another.
-- Dart Language Specification, Section 17.25, page 176
void t2(dynamic? v) {
var v1 = v ?? "test";
print(v1);
}
void main() {
t2(1);
t2(null);
}
$ dart run
Building package executable...
Built n:n.
t2: 1
t2: default value
void t3(dynamic? v) {
var v3 = null;
v3 ??= v;
print("t3: ${v3}");
}
void main() {
t3(1);
t3(null);
}
$ dart run
Building package executable...
Built n:n.
t3: 1
t3: null
void t4(dynamic? v) {
print(v?.toString());
}
void main() {
t4(1);
t4("test");
t4(null);
}
$ dart run
Building package executable...
Built n:n.
1
test
null
void t5(dynamic? v) {
print([...? v]);
print([... v]);
}
void main() {
t5([1]);
t5(["test"]);
t5(null);
}
$ dart run
Building package executable...
Built n:n.
[1]
[1]
[test]
[test]
[]
Unhandled exception:
type 'Null' is not a subtype of type 'Iterable<dynamic>'
#0 t5 (file:///home/user/tmp/dart/n/bin/n.dart:31:14)
#1 main (file:///home/user/tmp/dart/n/bin/n.dart:6:3)
#2 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:313:19)
#3 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12)
It's All About Syntactic Sugar
I consume too much sugar. It’s a problem, I need to stop.
-- Kevin Jonas
Most of the previous examples are syntactic sugar around the conditional expression. This can be seen in the Figure 3 in the 17.20 section of the Dart Language specification.
A conditional expression evaluates one of two expressions based on a boolean condition
-- Dart Language Specification, Section 7.24, page 175
Let check these expression by first defining a new class R containing one attribute id and one method f. Nothing complex there, it's just to have a custom object.
class R {
dynamic id = null;
R(dynamic this.id);
dynamic f(dynamic args) => args;
}
The first syntactic sugar to test is r?.attribute. If r is null then it will return null, else r.id will be used.
(dynamic, dynamic, bool) sugar1(dynamic r) {
var a = r?.id;
var b = r?.id == null ? null : r.id;
final bool res = a == b;
final ret = (a,b,res);
print("sugar1: $ret");
return ret;
}
The second one is the r?.attribute = e syntactic sugar. If r is null, nothing is set, but if it's not the case, the value e is set to the r.id attribute.
(dynamic, dynamic, dynamic) sugar2(dynamic r, dynamic e) {
var a = r?.id = e;
var b = r == null ? null : r.id = e;
final bool res = a == b;
final ret = (a,b,res);
print("sugar2: $ret");
return ret;
}
The third one is about method calling r?.f(args). If r is null then it will return null, else the method f() is called.
(dynamic, dynamic, dynamic) sugar3(dynamic r, dynamic args) {
var a = r?.f(args);
var b = r == null ? null : r.f(args);
final bool res = a == b;
final ret = (a,b,res);
print("sugar3: $ret");
return ret;
}
The fourth one is about List: r?[e]. If r is null then null is returned else, the index is used on the list r.
(dynamic, dynamic, dynamic) sugar4(dynamic r, dynamic e) {
var a = r?[e];
var b = r == null ? null : r[e];
final bool res = a == b;
final ret = (a,b,res);
print("sugar4: $ret");
return ret;
}
The fifth one is still about list, this time, if r is null, nothing can be done on the list, but if r is not null, the index e1 will be used to set the value e2.
(dynamic, dynamic, dynamic) sugar5(dynamic r, dynamic e1, dynamic e2) {
var a = r?[e1] = e2;
var b = r == null ? null : r[e1] = e2;
final bool res = a == b;
final ret = (a,b,res);
print("sugar5: $ret");
return ret;
}
Those functions can be added in our entry-point...
void main() {
sugar1(R(1));
sugar1(R(null));
sugar2(R(1), 2);
sugar2(R(null), 2);
sugar3(R(1), 2);
sugar3(R(null), 2);
sugar4([1,2,3], 0);
sugar4(null, 0);
sugar5([1,2,3], 0, 1);
sugar5(null, 0, 1);
}
... And finally invoked.
$ run bin/n.dart
sugar1: (1, 1, true)
sugar1: (null, null, true)
sugar2: (2, 2, true)
sugar2: (2, 2, true)
sugar3: (2, 2, true)
sugar3: (2, 2, true)
sugar4: (1, 1, true)
sugar4: (null, null, true)
sugar5: (1, 1, true)
sugar5: (null, null, true)
The results are similar. I know, this is a dummy test, something more accurate could have been done, but it was just to give the equivalent syntax one can use.
Conclusion
Nothing exists but atoms and the void.
-- Democritus
After a bit more than 1 month of coding with Dart, I found the null part exhausting. To me, this part of the language is close to the annoying error catching in Go... Perhaps a little bit better than that though. Adding more and more syntactic sugar to fix a problem like this one can lead to confusing syntax, and it seems to be the case. Using ?, ?? or ! in different part of the code makes everything more complex.
As usual, I am not the only one to write about this topic, and it would be a shape to not share all the resources I used to learn about null safety in Dart. Here the list:
Understanding null safety from the Official Dart documentation;
Sound null safety from the Official Dart Documentation;
Null Safety and Variables from the Official Dart Documentation;
Why nullable types? from the Official Dart Blog;
The road to Dart 3: A fully sound, null safe language from the Official Dart Blog;
Announcing Dart null safety beta on the Official Dart Blog;
Nullclass API documentation;Nullclass implementation source code at Github;Native NULL Assertions from Dart SDK on Github;
Null safety in Dart from leancode;
Dart Lesson 10: Null Safety (Part 1) — Why is Null Safety Needed? by @jige2025;
Dart Lesson 11: Null Safety (Part 2) — Safe Operators Explained by @jige2025;
Type Promotion and Nullability in Dart by Alexander Obregon on Substack;
Understanding Null Safety in Dart: Why It Matters and How to Use It by Ugama Benedicta Kelechi on Medium;
Dart: How to Write Shorter Null, False, Empty String, and Zero Checks from CodeStudy.net;
Mastering Dart's Null Safety: From ? to ! and Everything In Between by @hiteshm_devapp;
Master Null Safety in Dart: A Step-by-Step by Abhay Kumar Bhumihar on Medium.
STATIC TYPE CHECKER TOOLS FOR DART by Snigdha Mokkapati
Type Unsoundness in Practice: An Empirical Study of Dart by Gianluca Mezzetti, Anders Møller and Fabio Strocco
Type Soundness in the Dart Programming Language by Fabio Strocco
Have fun! And Hack well!
Top comments (0)