Contents

Unsound null safety

A Dart program may contain some libraries that are null safe and some that aren’t. These mixed-version programs rely on unsound null safety.

The ability to mix language versions frees package maintainers to migrate their code, with the knowledge that even legacy users can get new bug fixes and other improvements. However, mixed-version programs don’t get all the advantages that null safety can bring.

This page describes the differences between sound and unsound null safety, with the goal of helping you decide when to migrate to null safety. After the conceptual discussion are instructions for migrating incrementally, followed by details on testing and running mixed-version programs.

Sound and unsound null safety

Dart provides sound null safety through a combination of static and runtime checks. Each Dart library that opts in to null safety gets all the static checks, with stricter compile-time errors. This is true even in a mixed-version program that contains null-unsafe libraries. You start getting these benefits as soon as you start migrating some of your code to null safety.

However, a mixed-version program can’t have the runtime soundness guarantees that a fully null-safe app has. It’s possible for null to leak out of the null-unsafe libraries into the null-safe code, because preventing that would break the existing behavior of the unmigrated code.

To maintain runtime compatibility with legacy libraries while offering soundness to completely null-safe programs, Dart tools support two modes:

  • Mixed-version programs run with unsound null safety. It’s possible for null reference errors to occur at runtime, but only because a null or nullable type escaped from some null-unsafe library and got into null-safe code.

  • When a program is fully migrated and all its libraries are null safe, then it runs with sound null safety, with all of the guarantees and compiler optimizations that soundness enables.

Sound null safety is what you want if possible. Dart tools automatically run your program in sound mode if the main entrypoint library of your program has opted into null safety. If you import a null-unsafe library, the tools print a warning to let you know that they can only run with unsound null safety.

Migrating incrementally

Because Dart supports mixed-version programs, you can migrate one library (generally one Dart file) at a time, while still being able to run your program and its tests.

We recommend that you first migrate leaf libraries—libraries that don’t import other files from the package. Then migrate libraries that directly depend on the leaf libraries. End by migrating the libraries that have the most intra-package dependencies.

For example, say you have a lib/src/util.dart file that imports other (null-safe) packages and core libraries, but that doesn’t have any import '<local_path>' directives. Consider migrating util.dart first, and then migrating files that depend only on util.dart. If any libraries have cyclic imports (for example, A imports B which imports C, and C imports A), consider migrating those libraries together.

Using the migration tool

You can migrate incrementally using the migration tool. To opt out files or directories, click the green checkbox. In the following screenshot, all files in the bin directory are opted out.

Screenshot of file viewer in migration tool

Each opted out file will be unchanged except for a 2.9 language version comment. You can later run dart migrate again to continue the migration. Any files that are already migrated feature a disabled checkbox: you cannot un-migrate a file once it has been migrated.

Migrating by hand

If you want to incrementally migrate a package by hand, follow these steps:

  1. Edit the package’s pubspec.yaml file, setting the minimum SDK constraint to at least 2.12.0:

    environment:
      sdk: '>=2.12.0 <3.0.0'
    
  2. Regenerate the package configuration file:

    $ dart pub get
    

    Running dart pub get with a lower SDK constraint of 2.12.0 sets the default language version of every library in the package to 2.12, opting them all in to null safety.

  3. Open the package in your IDE.
    You’re likely to see a lot of analysis errors. That’s OK.

  4. Add a language version comment to the top of any Dart files that you don’t want to consider during your current migration:

    // @dart=2.9
    

    Using language version 2.9 for a library that’s in a 2.12 package can reduce analysis errors (red squiggles) coming from unmigrated code. However, unsound null safety reduces the information the analyzer can use. For example, the analyzer might assume a parameter type is non-nullable, even though a 2.9 file might pass in a null value.

  5. Migrate the code of each Dart file, using the analyzer to identify static errors.
    Eliminate static errors by adding ?, !, required, and late, as needed.

Testing or running mixed-version programs

To test or run mixed-version code, you need to disable sound null safety. You can do this in two ways:

  • Disable sound null safety using the --no-sound-null-safety flag to the dart or flutter command:

    $ dart --no-sound-null-safety run
    $ flutter run --no-sound-null-safety
    
  • Alternatively, set the language version in the entrypoint—the file that contains main() function—to 2.9. In Flutter apps, this file is often named lib/main.dart. In command-line apps, this file is often named bin/<packageName>.dart. You can also opt out files under test, because they are also entrypoints. Example:

    // @dart=2.9
    import 'src/my_app.dart';
    
    void main() {
      //...
    }
    

Opting out tests using either of these mechanisms can be useful for testing during your incremental migration process, but doing so means that you aren’t testing your code with full null safety enabled. It’s important to opt your tests back in to null safety when you’ve finished the incremental migration of your libraries.