Start Debugging

Solução: Null check operator used on a null value no Flutter

O operador ! encontrou um null em runtime. Substitua-o por ?. e ?? para um valor padrão, ou proteja com uma verificação de null explícita, em vez de afirmar um valor que não estava lá.

Você escreveu something! e something era null quando a linha rodou. O operador de verificação de null (bang) promete ao compilador “isto nunca é null”, e o Dart faz cumprir essa promessa em runtime lançando o erro no instante em que ela é quebrada. A solução quase sempre é parar de afirmar e começar a tratar: use ?. para curto-circuitar, ?? para fornecer um valor padrão, ou uma proteção if (x != null) que deixa o compilador estreitar o tipo por você. Esta página usa Flutter 3.44 (estável, maio de 2026) e Dart 3.x.

O erro em contexto

Quando o operador bang encontra um null, você recebe um TypeError com esta mensagem exata:

Unhandled Exception: Null check operator used on a null value

Na camada de widgets ele geralmente aparece envolvido pelo framework, que é onde a maioria das pessoas de fato o vê:

======== Exception caught by widgets library =======================================================
The following _TypeError was thrown building ProfilePage(dirty):
Null check operator used on a null value

The relevant error-causing widget was:
  ProfilePage ProfilePage:file:///lib/profile_page.dart:18:12

A classe é _TypeError (um subtipo de TypeError), a mesma família que o Dart usa para casts que falham. Essa é a pista: o operador bang é um cast. Ele converte T? para T, e como qualquer cast pode falhar em runtime.

Por que isso acontece: o operador bang é um cast verificado

Em null safety sólida, String? e String são tipos diferentes. O ! posfixo é uma forma abreviada que, nas palavras da documentação do Dart, “pega a expressão à esquerda e a converte para o seu tipo não anulável subjacente”. Um cast de um tipo anulável para um não anulável não pode ser provado como seguro em tempo de compilação, então o compilador insere uma verificação em runtime. Se o valor for null quando a verificação rodar, você recebe Null check operator used on a null value.

Então isso nunca é um bug do compilador nem do framework. É um valor sendo null em um momento em que o seu código jurou que ele não seria. O trabalho é encontrar o valor e decidir o que deve acontecer quando ele estiver genuinamente ausente, em vez de encobri-lo com outro !.

Reprodução mínima

A versão mais simples é uma única variável anulável à qual ainda não foi atribuído nada:

// Flutter 3.44, Dart 3.x
String? name;          // nullable, defaults to null
void main() {
  print(name!.length); // throws: Null check operator used on a null value
}

Em código Flutter real, o formato mais comum são dados que ainda não carregaram. O campo é null até uma chamada de rede preenchê-lo, mas build roda imediatamente e o desreferencia:

// Flutter 3.44, Dart 3.x -- crashes on first build
class ProfilePage extends StatefulWidget {
  const ProfilePage({super.key});
  @override
  State<ProfilePage> createState() => _ProfilePageState();
}

class _ProfilePageState extends State<ProfilePage> {
  User? _user; // null until the fetch returns

  @override
  void initState() {
    super.initState();
    _loadUser(); // async, completes some frames later
  }

  Future<void> _loadUser() async {
    final u = await api.fetchUser();
    setState(() => _user = u);
  }

  @override
  Widget build(BuildContext context) {
    return Text(_user!.name); // <-- _user is null on the first build
  }
}

build é chamado antes de _loadUser resolver, então _user ainda é null no primeiro frame e _user! lança o erro.

A solução, em ordem de preferência

A solução correta depende de null ser um estado legítimo (dados ainda carregando, campo opcional, chave ausente) ou um bug (você esperava um valor e a ausência dele significa que algo mais acima está quebrado). Na maioria das vezes é o primeiro caso, e o framework te dá ferramentas idiomáticas para isso.

1. Forneça um valor padrão com ??

Se existe um valor de fallback sensato, o operador de coalescência de null é a solução correta mais curta. Ele retorna o lado direito quando o esquerdo é null:

// Flutter 3.44, Dart 3.x
Text(_user?.name ?? 'Loading...');

_user?.name é null quando _user é null (o ?. curto-circuita toda a cadeia), e ?? substitui o marcador. Sem lançamento de erro, e a interface mostra algo útil enquanto os dados carregam.

2. Ramifique no estado de carregamento explicitamente

Quando não há um bom valor padrão, renderize widgets diferentes para o estado carregado e o ainda-não-carregado. Uma verificação if (x != null) promove a variável local para não anulável dentro do ramo, então você não precisa de ! algum:

// Flutter 3.44, Dart 3.x
@override
Widget build(BuildContext context) {
  final user = _user;            // copy to a local for promotion
  if (user == null) {
    return const Center(child: CircularProgressIndicator());
  }
  return Text(user.name);        // user is User here, not User?
}

Copie o campo para uma variável local primeiro. O Dart só promove variáveis locais, não campos de instância, porque outro método (ou outro isolate) poderia mutar um campo entre a verificação e o uso. A variável local é a parte que sustenta esse padrão.

3. Deixe o FutureBuilder cuidar do estado null

Se o valor vem de uma única chamada assíncrona, não improvise o flag na mão. O FutureBuilder modela carregamento, erro e dados como um único objeto, e você só lê data depois de confirmar que ele está presente:

// Flutter 3.44, Dart 3.x
FutureBuilder<User>(
  future: _userFuture, // created once, not in build -- see below
  builder: (context, snapshot) {
    if (snapshot.connectionState != ConnectionState.done) {
      return const CircularProgressIndicator();
    }
    if (snapshot.hasError) {
      return Text('Failed: ${snapshot.error}');
    }
    return Text(snapshot.data!.name); // safe: hasData is implied here
  },
);

O snapshot.data! aqui é legítimo porque você já provou que o future completou sem erro. Uma ressalva que pega muita gente: crie o future uma única vez e armazene-o, nunca inline dentro de build, ou cada reconstrução inicia um novo fetch. Esse é um problema próprio, coberto em por que o FutureBuilder fica recriando o seu Future.

4. Use late somente quando a inicialização realmente precede a primeira leitura

Se um valor é atribuído exatamente uma vez, antes de qualquer coisa lê-lo, late remove a nulabilidade sem o bang. Mas isso é uma troca, não um ganho grátis: um campo late lido antes da atribuição lança LateInitializationError, um crash diferente e possivelmente pior, porque é fácil supor que late tornou o valor seguro. Recorra a ele apenas quando a ordem é garantida, por exemplo um valor definido em initState e lido em build:

// Flutter 3.44, Dart 3.x
late final AnimationController _controller;

@override
void initState() {
  super.initState();
  _controller = AnimationController(vsync: this); // assigned before any build
}

Se a ordem não é garantida, mantenha o campo anulável e proteja-o. O detalhamento completo de quando late ajuda e quando atrapalha está em como resolver o LateInitializationError no Flutter.

Os suspeitos de sempre além de um flag de carregamento

O erro se disfarça de várias formas. Estas são as de maior frequência, cada uma com a mesma causa subjacente e o mesmo formato de solução.

GlobalKey.currentState! antes de o widget estar montado. Chamar _formKey.currentState!.validate() quando o Form não está na árvore (ou ainda não foi construído) lança o erro, porque currentState é null até o widget se anexar. Use ?.:

// Flutter 3.44, Dart 3.x
if (_formKey.currentState?.validate() ?? false) {
  // form is valid and present
}

Argumentos de rota que não foram passados. ModalRoute.of(context)!.settings.arguments as Args assume tanto que existe uma rota quanto que argumentos foram fornecidos. Se você empurra a rota sem argumentos, arguments é null e o as posterior ou um ! seguinte explode. Leia de forma defensiva:

// Flutter 3.44, Dart 3.x
final args = ModalRoute.of(context)?.settings.arguments as Args?;
if (args == null) return const ErrorScreen('Missing arguments');

Acesso a Map e JSON com [key]!. Uma busca em um map retorna null para uma chave ausente, e json['email']! lança o erro no momento em que o campo está ausente ou a API o renomeou. Decodifique através de um modelo com nulabilidade explícita ou atribua um valor padrão a cada campo:

// Flutter 3.44, Dart 3.x
final email = (json['email'] as String?) ?? '';

firstWhere com um bang sobre o resultado. Às vezes as pessoas escrevem list.firstWhere((e) => e.id == id, orElse: () => null)! para “encontrar ou estourar”. Isso é exatamente uma suposição não verificada. Prefira firstWhereOrNull do package:collection e trate o caso vazio:

// Flutter 3.44, Dart 3.x
final match = list.firstWhereOrNull((e) => e.id == id);
if (match == null) { /* handle not found */ }

Variantes que isto não é, e para onde ir em vez disso

O tráfego de busca para esse erro muitas vezes pertence a uma página vizinha. Três parecidos:

LateInitializationError: Field '_x' has not been initialized é um irmão, não o mesmo erro. Ele vem de ler uma variável late antes da atribuição, não de um ! sobre um anulável. Se o seu stack trace diz LateInitializationError, a solução está na página do LateInitializationError, não aqui.

Uma verificação de null que só falha depois de navegar, e só em release, costuma ser um sintoma de contexto morto. A busca sobre um contexto desativado retorna null em builds de release (o assert que o detecta é só de debug), e esse null então dispara um ! em algum ponto mais abaixo. Se o crash se correlaciona com um await seguido de um uso do contexto, leia usar BuildContext com segurança depois de um await, porque o erro real está acima do bang.

Um TextEditingController ou outro controlador usado depois de dispose também pode alimentar um null para uma afirmação posterior. Se o controlador é a fonte, como resolver o erro de controlador descartado aborda o ciclo de vida diretamente.

O lint que pega isso antes do runtime

O Dart não pode avisar sobre cada ! que possa falhar, porque esse é justamente o sentido do operador: você está sobrepondo o analisador. Mas ele pode sinalizar os que consegue provar que são inúteis. A regra unnecessary_non_null_assertion, ativada por padrão no flutter_lints, dispara quando você aplica um bang a um valor que o analisador já sabe ser não nulo, o que normalmente significa que o seu modelo mental e o sistema de tipos não concordam:

# analysis_options.yaml -- on by default via flutter_lints
include: package:flutter_lints/flutter.yaml

A disciplina mais ampla é tratar cada ! que você digita como uma afirmação que você precisa defender. Se você não consegue apontar a linha que garante que o valor é não nulo naquele caminho, você não tem um !, você tem um Null check operator used on a null value latente. Modelar os dados assíncronos como estados explícitos de carregamento e erro, como em estados de carregamento e erro com AsyncValue, elimina a maioria dessas afirmações por completo, porque o framework entrega o valor só no ramo onde ele existe.

O hábito que aposenta o erro

O operador bang é uma promessa ao compilador, quitada em runtime, e Null check operator used on a null value é o recibo de uma promessa quebrada. Sempre que você for tentado a escrever !, pergunte-se o que deve acontecer quando o valor for realmente null: um marcador (??), um widget diferente (if (x != null)), ou um erro real que você levanta de propósito. Escolha uma dessas opções e o crash nunca chega a um usuário. Reserve ! para o caso raro em que null seria uma violação genuína de uma invariante, e mesmo assim, prefira lançar um StateError com uma mensagem que explique o que deu errado a um bang pelado que diz apenas “isto era null”.

Fontes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Voltar