flutter
futurebuilder
snapshot
instance of object
debugging

Flutter FutureBuilder snapshot returns Instance of 'Object' instead of data

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

When a Flutter screen shows Instance of 'Object', the problem is usually in the rendering code, not in FutureBuilder itself. Dart is simply calling the default string representation for a model object. The fix is to keep the future strongly typed, parse API results into a real model, and render properties instead of dumping the entire object.

Why Instance of 'Object' Appears

If a class does not override toString(), interpolating that object in text gives a generic result. That means code such as Text('${snapshot.data}') often produces an object label instead of useful UI.

FutureBuilder is only exposing the future result. The confusing text appears because the widget tree is asking Dart to convert a custom object to a string.

Use a Strongly Typed FutureBuilder

The first fix is to stop treating the future result as a generic object. Make the future return a concrete model and type the builder accordingly.

dart
1import 'package:flutter/material.dart';
2
3class User {
4  final int id;
5  final String name;
6
7  User({required this.id, required this.name});
8}
9
10Future<User> fetchUser() async {
11  await Future<void>.delayed(const Duration(milliseconds: 300));
12  return User(id: 1, name: 'Ava');
13}
14
15class UserScreen extends StatelessWidget {
16  const UserScreen({super.key});
17
18  
19  Widget build(BuildContext context) {
20    return FutureBuilder<User>(
21      future: fetchUser(),
22      builder: (context, snapshot) {
23        if (snapshot.connectionState != ConnectionState.done) {
24          return const Center(child: CircularProgressIndicator());
25        }
26
27        if (snapshot.hasError) {
28          return Text('Error: ${snapshot.error}');
29        }
30
31        if (!snapshot.hasData) {
32          return const Text('No data');
33        }
34
35        final user = snapshot.data!;
36        return Text('User name: ${user.name}');
37      },
38    );
39  }
40}

Now the UI renders a field from the model instead of whatever the default object formatter happens to return.

Parse API Data Into a Model

This issue often starts earlier in the flow. If API code returns raw maps or untyped dynamic values, the UI layer has to guess what snapshot.data contains. Parsing into a model makes the contract explicit.

dart
1class User {
2  final int id;
3  final String name;
4
5  User({required this.id, required this.name});
6
7  factory User.fromJson(Map<String, dynamic> json) {
8    return User(
9      id: json['id'] as int,
10      name: json['name'] as String,
11    );
12  }
13}
14
15Future<User> fetchUserFromApi() async {
16  await Future<void>.delayed(const Duration(milliseconds: 300));
17  final response = <String, dynamic>{
18    'id': 7,
19    'name': 'Mina',
20  };
21  return User.fromJson(response);
22}

Once the async layer returns User, the widget code becomes simple and predictable.

Override toString() for Debugging, Not UI

Overriding toString() can make logs and debug output much more useful. It is still not the best main strategy for user-facing text because most screens need formatted fields, labels, and layout rather than one raw object string.

dart
1class User {
2  final int id;
3  final String name;
4
5  User({required this.id, required this.name});
6
7  
8  String toString() => 'User(id: $id, name: $name)';
9}

This helps when printing snapshot.data during development, but production UI should still render user.name, user.id, or some purpose-built display model.

Keep the Future Stable Across Builds

Another common source of confusion is recreating the future every time build runs. That does not directly cause Instance of 'Object', but it makes async state harder to reason about and can repeatedly trigger network calls.

dart
1class StableUserScreen extends StatefulWidget {
2  const StableUserScreen({super.key});
3
4  
5  State<StableUserScreen> createState() => _StableUserScreenState();
6}
7
8class _StableUserScreenState extends State<StableUserScreen> {
9  late final Future<User> _futureUser;
10
11  
12  void initState() {
13    super.initState();
14    _futureUser = fetchUser();
15  }
16
17  
18  Widget build(BuildContext context) {
19    return FutureBuilder<User>(
20      future: _futureUser,
21      builder: (context, snapshot) {
22        if (!snapshot.hasData) {
23          return const SizedBox.shrink();
24        }
25        return Text(snapshot.data!.name);
26      },
27    );
28  }
29}

Holding the future in state makes rebuilds predictable and reduces noise while debugging.

Common Pitfalls

  • Writing Text('${snapshot.data}') instead of rendering a property from the model.
  • Using dynamic or Object where a concrete model type should be used.
  • Returning raw JSON maps from the data layer and pushing parsing into the widget tree.
  • Relying on toString() as a UI solution instead of a debugging aid.
  • Creating a new future on every build and making async behavior look inconsistent.

Summary

  • 'Instance of 'Object' usually means a raw object is being converted to text.'
  • Type FutureBuilder with the real model type and render model fields directly.
  • Parse JSON into model classes before the UI layer.
  • Override toString() only to improve debugging output.
  • Keep the future stable when the request should run once per screen load.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions