It was 1:30 AM. I was staring at the screen, wondering “Why is this value changing?”
I didn’t write code to change it. I was sure. But somehow… it did.
That’s when I realized
I used late
.
But I should’ve used late final
.
The Context: A Simple User Controller
Let me explain.
I had a controller that fetched user data from the server when the app started. Here’s what it looked like:
class UserController {
late User user;
Future<void> loadUser() async {
user = await userService.getCurrentUser();
}
}
So far, so good.
Now in the rest of the app, I used userController.user.name
, userController.user.email
, etc. confidently assuming this value would stay the same.
But I made one tiny mistake:
I used late
instead of late final
.
The Bug: Value Got Replaced Silently
After a few days, a new developer joined and added a “Refresh Profile” button that called loadUser()
again.
So, what happened?
The user
variable got replaced with new data, and in some parts of the app things started breaking subtly:
- Analytics logs had inconsistent user IDs.
- Cache was overwritten.
- Some values were missing.
It wasn’t a big crash just small, silent chaos.
And it took me hours to figure out why.
The Root Cause: late
is Not Read-only
When we write:
late User user;
We’re saying:
“I will assign this later, but I can assign it again too.”
So, this is valid Dart code:
late String name;
name = 'Alamin';
name = 'Karno'; // ✅ No error!
But I didn’t want that.
In my case, the user
should be assigned once and then locked.
The Fix: late final
I updated my code to:
class UserController {
late final User user;
Future<void> loadUser() async {
user = await userService.getCurrentUser();
}
}
Now if someone tries this:
user = anotherUser; // ❌ BOOM! Compilation error!
Dart says:
“Nope. This is final. You already assigned it once.”
And I sleep peacefully now.
Real Flutter Example
Imagine you’re building a screen that depends on one-time loaded data like this:
class ProductController {
late final Product product;
Future<void> fetchProduct() async {
product = await api.getProduct();
}
}
In your widget:
Text(productController.product.name)
If you mistakenly used late
, someone could reassign it later and the UI would update with incorrect info or worse, inconsistent app behavior.
A Quick Comparison
- `var - Regular values that can change
-
final
- Fixed values known immediately -
late
- Delayed but changeable (risky!) -
late final
- Delayed and locked after 1st set
When to Use late final
Use late final
when:
- The value will be set later (e.g., async calls,
initState
, etc.) - It must not change again.
- You want to protect your variable from accidental reassignment.
Friendly Advice
If you ever write this:
late SomeType something;
Ask yourself:
“Will this value change after it’s set?”
If the answer is ❌ No
Just write:
late final SomeType something;
It’s safer. Cleaner. And future you will thank you.
Final Words
We don’t always break our app with big mistakes.
Sometimes, it’s just one small word:
late
instead of late final
.
From that day on, I started using late final
in almost all of my controllers, models, and services unless I really needed to change the value later (which is rare).
So yeah, don’t ignore this little trick. It might just save your next release.