Tomáš Repčík - 13. 7. 2025

Stop Using Null Assertions in Flutter

Alternatives to Null Assertions in Dart

Bang

In most programming languages, we have some kind of nullability. A variable does not have any value, or it is not initialised.

In many languages, there is some equivalent of a null pointer exception, which is a runtime error that occurs when you try to access a property or method of a null object.

In Dart, we have null safety, which is a feature that helps us avoid null pointer exceptions. Even during development, it will warn us about the nullability of a variable.

However, I see a lot of developers using null assertions (!) to bypass null safety. It is a way to tell the compiler that you are sure that the variable is not null, even if it is not initialised or it can be null.

Even though it is a feature of the language, I think it is a bad practice to use it. It can lead to runtime errors, and it defeats the purpose of null safety.

I heard some people call it bang operator for a reason. It can ruin your intentions and lead to unexpected behavior.

There are a plethora of alternatives to null assertions, which can help you to avoid null pointer exceptions and make your code more robust.

Here is a run-down of them:

All examples are in Dart with version 3.8.x - the latest stable version at the time of writing.

Dealing with nullability in methods

Firstly, let’s define a simple object which will appear in our examples. It is a simple configuration object with two nullable properties: domain and port.

class Config {
  final String? domain;
  final int? port;   

  const Config({this.domain, this.port});
}

Null prohibits you from program execution

This is the simplest scenario.

Let’s say you have a method that takes a nullable parameter and you want to ensure that the parameter is not null before proceeding with the execution.

Response processConfig(Config? config) {
  ...
} 

You have a couple of options for what to do with the config parameter.

You can use a simple if statement to check if the config is null and throw an exception or return a default value.

Response processConfig(Config? config) {
  if (config == null) {
    return Response.noConfig('No config given');
  }
  // proceed with config
}

Afterwards, the compiler will know that config is not null and you can safely access its properties.

You should not attempt to throw an exception in such case, because it is not an exceptional situation. It is a normal flow of the program and program should handle it gracefully.

Alternatively, you can use a default value in the input parameter, ensuring it always has a value regardless of what you pass.

Response processConfig({Config config = const Config()}) {
  // proceed with config
}

Default value must be a constant.

Nullable properties in variables

Firstly, a variable can be, null and you want to access its properties safely.

Response processConfig(Config? config) {
  client.connect(
    domain: config?.domain ?? 'localhost',
    port: config?.port ?? 8080,
  );
}

Here is a combination of the null-aware operator ?. and the null-coalescing operator ??.

The null-aware operator ?. is used to access the domain and port properties of the config object only if it is not null. If it is null, it will return null.

Then we use the null-coalescing operator ?? to provide a default value if the property is null. In this case, if config?.domain is null, it will use 'localhost' as the default value, and if config?.port is null, it will use 8080.

If you are after more verbose code, another viable option is to use the ternary operator to check if the variable is null and return a default value.

Response processConfig(Config? config) {
  client.connect(
    domain: config != null ? config.domain : 'localhost',
    port: config != null ? config.port : 8080,
  );
}

Nullable properties initialisation

If you want to initialize a property only if it is null, you can use the null-aware assignment operator ??=.

class User {

  String? _userName

  String get userName {
    final name = _generateUserName();
    _userName ??= name; 
    return name;
  }

}

The null-aware assignment operator ??= is used to assign a value to the _userName property only if it is null. If _userName is already set, it will not change its value.

It is useful when you want to ensure that the variable has a value, but you do not want to throw an exception or return a default value.

Nullable properties in lists

Only non-null values

Imagine you have a list of domains, and some of them are null. You want to filter out the null values and use the rest.

List<String?> domains = ['example.com', null, 'test.com', null];
List<String> filteredDomains = domains.nonNulls().toList();

Before, it used to be .whereType<String>() to filter out the null values, but now you can use the nonNulls() extension method.

During the initialisation of iterables

You can use the null-aware operator ? to conditionally add an item to a list if it is not null.

final String domain1 = 'example.com';
final String domain2 = null;
final String domain3 = 'test.com';
List<String> domains = [domain1, ?domain2, domain3];

The resulting list will contain only ['example.com', 'test.com'].

There is no need to use the if statement to check if the variable is null, because the null-aware operator ? will do it for you.

Merging nullable lists

If you have two lists and you want to merge them, but some of the items can be null, you can use the nonNulls() extension method to filter out the null values.

List<String?> domains1 = ['example.com', null, 'test.com'];
List<String?> domains2 = [null, 'example.org', 'test.org'];
List<String> mergedDomains = [...domains1, ...domains2].nonNulls().toList();

Or the whole list can be nullable, and if you want to merge it with another list, you can use the null-aware spread operator ...? to conditionally spread the list if it is not null.

List<String?>? domains1 = ['example.com', null, 'test.com'];
List<String?> domains2 = [null, 'example.org', 'test.org'];
List<String> mergedDomains = [...?domains1, ...domains2].nonNulls().toList();

Guard clauses and Pattern matching

Guard clauses are a way to handle the nullability of the variable at the beginning of the method.

The first example shown in this article is also an example of a guard clause.

Now, we will use pattern matching.

Response processUserConfig(Config? config) {
  if (config case final Config currentConfig) {
    // proceed with currentConfig
  } else {
    return Response.noConfig('No config given');
  }
}

Since pattern matching is available, you can also use such syntax in the if statement to check if the variable is not null and assign it to a new variable. However, I prefer to use simple if ( ... != null).

If you have more complex logic where you can take multiple routes, then pattern matching is becoming much more useful, mainly thanks to the when clause.

Response processUserConfig(Config? config) {
  switch (config) {
    case final Config currentConfig when currentConfig.domain != null:
      // proceed with config with non-null domain
    case final Config currentConfig:
      // proceed with config with null domain
    default:
      return Response.noConfig('No domain given');
  }
}

Suddenly, you can handle the nullability of the variable more elegantly and handle it with multiple cases and conditions.

The same example applies to sealed classes, where you can use the when clause to handle the nullability of the variable in a more elegant way.

sealed class Config {}

class LocalConfig extends Config {}

class UserConfig extends Config {
  final int port;
  final String? domain;

  UserConfig({required this.domain, required this.port});
}

Response processUserConfig(Config? config) {
  switch (config) {
    case UserConfig(:final domain) when domain != null:
      // proceed with config with non-null domain
    case LocalConfig():
      // proceed with local host
    default:
      return Response.noConfig('No config given');
  }
}

This way, you can nitpick the nullability of each variable in the sealed class and handle the case accordingly. Moreover, you see all the possible cases in one place, and you can always tweak the behaviour of the method without changing the method signature.

Nullability in widgets

All the examples above apply to widgets as well. You can use the null-aware operator ? to conditionally render a widget if it is not null.

Null-aware operator in widgets

class MyWidget extends StatelessWidget {
  final Widget? widget;

  const MyWidget({Key? key, this.widget}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Hello World'),
        ?widget
      ],
    );
  }
}

Null-aware properties

You can access nullable properties of widgets, like for theme colour or text style.

class MyWidget extends StatelessWidget {
 
  const MyWidget({Key? key) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(
      'Hello World',
      style: Theme.of(context).textTheme.bodyMedium ?? TextStyle(
        color: Colors.black,
        fontSize: 16,
      ),
    );
  }
}

Or you can go further in properties and use the null-aware operator ?. to conditionally render a widget if it is not null.

class MyWidget extends StatelessWidget {
 
  const MyWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(
      'Hello World',
      style: Theme.of(context).textTheme.bodyMedium?.copyWith(
        color: Colors.black,
        fontSize: 16,
      ),
    );
  }
}

Pattern matching in widgets

You can use pattern matching in widgets as well. For example, you can use it to handle the nullability of the widget and render a different widget based on its type.

class MyWidget extends StatelessWidget {
  final Config? config;

  const MyWidget({Key? key, this.config}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return switch (config) {
      UserConfig(:final domain) when domain != null => Text('User config with domain: $domain'),
      LocalConfig() => Text('Local config'),
      _ => Text('No config given'),
    };
  }
}

When to use null assertions?

Personally, I do not use null assertions at all. Only during development, when I want to quickly fail some method as a test or prove some issue I am trying to solve.

As you can see, there are plenty of alternatives to null assertions, which can help you to avoid null pointer exceptions and make your code more robust.

I think it is a bad practice to use them in production code, because it can lead to runtime errors and defeat the purpose of null safety.

Thanks for reading! If you have any questions or comments, feel free to reach out.

Subscribe for more
LinkedIn GitHub Medium X