In Dart, what does the postfix ! operator do at run time, and why does it hand the null risk back to you?
answer
- a cast, not a check you can skip
- String? to String
- TypeError on null
- 'Null check operator used on a null value'
- T? x with nullable T: use as T
basics
~20 sIn Dart, the postfix ! casts a nullable expression to its non-nullable type; if the value is null it throws a TypeError, 'Null check operator used on a null value'. The compiler stops checking, so a wrong assumption becomes a runtime crash.
solid answer
~50 s`value!` is shorthand for casting away nullability: `String? name` becomes a `String`, just like `name as String`. The compiler accepts it on faith, and to keep null safety sound the runtime checks it: if `name` is `null`, it throws a `TypeError` whose message is "Null check operator used on a null value" — a message Flutter developers know from the red error screen. That's what handing the risk back means: without `!` the analyzer would have forced you to handle `null`; with it, the case is still there but now surfaces as a crash in production, far from where the wrong assumption was made. `!` is justified when you know an invariant the type system cannot express, and the analyzer flags a pointless one with `unnecessary_non_null_assertion`. With generics, `T? x; x!` is wrong when `T` itself may be nullable — use `x as T`.
code
dart · 32 linesclass Response {
Response.ok() : code = 200, error = null;
Response.notFound() : code = 404, error = 'Not found';
final int code;
final String? error; // non-null exactly when code != 200
@override
String toString() {
if (code == 200) return 'OK';
// An invariant the type system cannot see; a message beats a bare !.
final message = error ?? (throw StateError('failed response without error'));
return 'ERROR $code ${message.toUpperCase()}';
}
}
T run<T>(T Function() compute) {
T? result;
(() {
result = compute();
})();
return result as T; // not result!: null may be a valid T
}
String? lookupNickname() => null;
void main() {
print(Response.notFound()); // ERROR 404 NOT FOUND
print(run<String?>(() => null)); // null, no TypeError
final name = lookupNickname();
print(name!.length); // throws TypeError: Null check operator used on a null value
}go deeper
Recall that ! turns T? into T and throws a TypeError, 'Null check operator used on a null value', when the value is actually null.
Explain that ! is a runtime-checked cast, why soundness requires the check, and the generic case where as T is correct instead.
Show you treat each ! as a claim to justify, replace it with promotion, ?? defaults or descriptive throws, and trace red-screen crashes back to the wrong assumption.
Set a team stance on !: where it is allowed, how invariants are documented, and how reviews keep its count from creeping back up.
## What ! does The postfix **`!`** — the *not-null assertion* or "bang" operator — takes an expression of nullable type and gives it the corresponding **non-nullable** type. For `String? name`, `name!` has type `String`, so `name!.length` compiles. It is a **cast**. The Dart docs describe `name!` as shorthand for `name as String`, useful when the underlying type is long, e.g. `Map<String, List<Set<int>>>`. Like every downcast, it moves the check from compile time to run time. ## What happens at run time Dart's null safety is **sound**, so the runtime must make sure a `String` expression never produces `null`. Therefore `!` is **always checked**: - if the value is non-null, it passes through unchanged; - if it is `null`, Dart throws a **`TypeError`** with the message **"Null check operator used on a null value"**. In a Flutter app it is a familiar sight on the red error screen of debug builds, often with a stack trace pointing deep into a `build` method. After `name!` on a local variable, flow analysis also **promotes** `name` to `String` for the following code, since execution can only continue if the check passed. ## Why it hands the risk back Null safety's value is that the analyzer refuses to let you forget `null`. `!` tells the analyzer "trust me", and it complies. The failure mode does not disappear: 1. **It moves in time** — from an analysis error you fix before shipping to an exception a user hits. 2. **It moves in place** — the crash occurs at the `!`, while the real mistake (why is this null?) is often elsewhere: a missing server field, a widget built before its data arrived, a callback that ran late. 3. **It outlives its reason** — a `!` that was safe when written keeps compiling after a refactor makes the value genuinely nullable. ## When ! is legitimate - An **invariant the type system cannot express**, established nearby. The Dart docs' example: a response class with `code` and a `String? error` that is non-null exactly when `code != 200`. - **Tests**, where a crash is the desired signal. Even then, `?? (throw StateError('why this must not be null'))` usually beats `!`: same crash, but with a message that names the broken invariant. Flow analysis also often makes `!` unnecessary — checking a local first promotes it. ## Pitfalls interviewers probe | Pitfall | Why it bites | Better | |---|---|---| | `widget.callback!()` for an optional callback | crashes when the caller passed none | `widget.callback?.call()` | | `json['name']!` on server data | trusts input you do not control | validate at the boundary, report bad data | | `x!` right after `if (x != null)` on a local | redundant; `unnecessary_non_null_assertion` warns | rely on promotion | | `result!` where `result` is `T?` in a generic | if `T` is nullable, `null` is a valid `T`, yet `!` throws | `result as T` (`null_check_on_nullable_type_parameter` lint) | | `!` on an expression that is always `null` | always throws | the analyzer reports `null_check_always_fails` | The generic case deserves a sentence of its own: with `T run<T>(T Function() f)`, a caller may instantiate `T` as `String?`. A `T? result` that holds `null` is then a legitimate `T`, and `result!` throws on it; `result as T` checks the right thing. The `null_check_on_nullable_type_parameter` lint is in the core, recommended and flutter sets.
- In Dart, why is value ?? (throw StateError('...')) often preferred over value! for an invariant?Both crash on `null`, but the `throw` carries a message naming the broken invariant and a more specific error type, which makes the crash report actionable. Because a throw expression has type `Never`, the whole expression still has the non-nullable type. Parenthesise the `throw` on the right of `??`.
- In Dart, does value! do anything when value is declared with a non-nullable type?Nothing useful: the value cannot be `null`, so the check never fails, and the analyzer reports `unnecessary_non_null_assertion`. Removing it is safe and makes the remaining `!` in the file meaningful.
! is like signing a waiver at the door: the staff stop checking whether you can swim, but the pool is just as deep. If you were wrong, the consequence arrives later, in the water.
saying these in an interview costs you the question
- ! converts null into a safe default value for the type.
- ! is checked only at compile time and costs nothing at run time.
- With result of type T?, result! is always right to get a T.
- A null ! throws a NoSuchMethodError when you call the member.
- Once code compiles with !, null safety guarantees it cannot crash on null.