DEV Community

marco menegazzi
marco menegazzi

Posted on

Your Flutter Logging Strategy is Embarrassing (Here's How to Fix It)

The Current State of Flutter Logging (It's Bad)

I reviewed 50+ Flutter projects on GitHub last week. Want to know what I found? Every single one was using print() statements as their primary debugging strategy.

Here's what "professional" Flutter logging looks like in 2025:

void handlePayment(double amount) {
  print('Processing payment: \$${amount}');

  try {
    final result = processPayment(amount);
    print('Payment successful!');
  } catch (e) {
    print('Error: $e'); // Good luck finding this in prod
  }
}
Enter fullscreen mode Exit fullscreen mode

If this looks familiar, you're part of the problem.

Why print() is Sabotaging Your Career

1. Production Invisibility

Your print() statements disappear the moment you deploy. When your app crashes at 3 AM and your manager is asking questions, those debug prints won't save you.

2. Performance Killer

Every print() call blocks the UI thread. You're literally making your app slower with every forgotten debug statement.

3. Zero Organization

Everything dumps to the same console. Finding relevant logs is like finding a specific grain of sand on a beach.

4. Memory Waste

Those string interpolations create objects that need garbage collection, even when logging is disabled.

5. Amateur Hour

Nothing screams "junior developer" like production code full of print() statements.

Building a Real Logging System

When I built the logging system for Mosaic, I didn't just wrap print() in a class. I engineered a production-ready system that handles real-world requirements.

Multi-Destination Logging

Different logs need different destinations:

await logger.init(
  dispatchers: [
    ConsoleDispatcher(),           // Dev debugging
    FileLoggerDispatcher(),        // Persistent logs  
    RemoteLogDispatcher(),         // Production monitoring
    SlackDispatcher(),             // Critical alerts
  ],
);
Enter fullscreen mode Exit fullscreen mode

When network fails, file logging continues. When disk fills up, remote logging keeps working.

Hierarchical Level System

Stop thinking binary (log or don't). Use levels:

// Development: Everything
logger.setLogLevel(LogLevel.debug);

// Staging: Skip noise
logger.setLogLevel(LogLevel.info);

// Production: Problems only
logger.setLogLevel(LogLevel.warning);
Enter fullscreen mode Exit fullscreen mode

Level filtering happens before processing, so higher levels = better performance.

Tag-Based Organization

Organize logs by context, not chaos:

logger.info('Login successful', ['auth', 'user_$id']);
logger.warning('Slow API: ${ms}ms', ['api', 'performance']);  
logger.error('DB connection lost', ['database', 'critical']);
Enter fullscreen mode Exit fullscreen mode

Now you can filter by component, user, or severity without parsing garbage.

Thread Safety That Actually Works

Most logging libraries aren't actually thread-safe. Multiple async operations logging simultaneously = corrupted output or race conditions.

My solution uses semaphore-based locking:

class ConsoleDispatcher extends LoggerDispatcher {
  final _semaphore = Semaphore();

  @override
  Future<void> log(String message, LogType type, List<String> tags) async {
    try {
      await _semaphore.lock();
      debugPrint(message); // Actually thread-safe now
    } finally {
      _semaphore.release();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Every dispatcher operation is mutex-protected. No more corrupted logs. You can also create your own dispatcher.

Rate Limiting Prevents Log Flooding

Your app hits an error loop and generates 50,000 logs per second. Congratulations, you just DoS'd yourself.

Real logging has rate limiting:

final _rateLimiter = RateLimiter(
  window: Duration(seconds: 1),
  maxRate: 1000,
);

if (_rateLimiter.exceeded()) {
  debugPrint('Rate limit hit, dropping message');
  return;
}
Enter fullscreen mode Exit fullscreen mode

Protects performance while ensuring legitimate messages get through.

Async Processing Without Pain

Every log operation is async to avoid UI blocking:

Future<void> _dispatch(String message, LogType type, List<String> tags) async {
  await Future.microtask(() async {
    await Future.wait(
      _dispatchers.values.map((d) => _safeDispatch(d, message, type, tags)),
    );
  });
}
Enter fullscreen mode Exit fullscreen mode

But here's the key—bulletproof error handling:

Future<void> _safeDispatch(LoggerDispatcher dispatcher, ...) async {
  try {
    await dispatcher.log(message, type, tags)
        .timeout(Duration(seconds: 5));
  } catch (e) {
    debugPrint('Dispatcher ${dispatcher.name} failed: $e');

    // Auto-disable broken dispatchers
    if (e is TimeoutException || e is IOException) {
      dispatcher.active = false;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

When dispatchers fail, logging continues. When network times out, file logging keeps working.

The Loggable Mixin

Instead of dependency injection everywhere, use a mixin with automatic context:

class PaymentService with Loggable {
  @override
  List<String> get loggerTags => ['payment'];
  // Each log has this tag by default

  Future<void> processPayment(double amount) async {
    info('Processing payment: \$${amount}'); // Auto-tagged

    try {
      await _stripe.charge(amount);
      info('Payment successful');
    } catch (e) {
      error('Payment failed: $e'); // Stands out as critical
      rethrow;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Every message automatically includes class context. DRY, clean, scalable.

Memory Management That Won't Leak

Proper cleanup prevents production memory leaks:

Future<void> dispose() async {
  if (_disposed) return;
  _disposed = true;

  // Clean up all dispatchers
  await Future.wait(
    _dispatchers.values.map((d) => d.dispose().catchError((_) {})),
  );

  // Clear all collections
  _dispatchers.clear();
  _defaultTags.clear();
  _tags.clear();
}
Enter fullscreen mode Exit fullscreen mode

Every resource tracked and cleaned up. No leaks, no surprises.

Stop Embarrassing Yourself

While you're debugging with print() like it's 1995, professional developers use logging systems that actually work in production.

Systems with:

  • Proper level filtering
  • Tag-based organization
  • Thread safety
  • Rate limiting
  • Error recovery
  • Memory management

The complete logging implementation is available in the Mosaic framework along with modular architecture, thread-safe utilities, and other tools that separate professionals from hobbyists.

dependencies:
  mosaic: ^0.0.3
Enter fullscreen mode Exit fullscreen mode

Your future self (and your team) will thank you.


What's your biggest logging pain point? Drop a comment and let's discuss how to solve it properly.

Top comments (0)