Developer logs

The first thing a developer does when things go wrong is add a print statement somewhere in the code and then re-run the app. This approach tends to work in the moment, but if you're not careful, you'll accumulate lots of random print statements throughout your codebase. Those print statements will turn into noise for the team, they might expose sensitive data, and they could even interfere with performance. You can use loggers to avoid these problems.

What is a logger?

A logger is essentially a print statement with more control.

You can turn a logger on and off, so you get the log messages when you want them, and don't get those log messages when you don't. This means you can turn off all of your loggers in production builds, which ensures that sensitive data is NOT written to output sources on end-user devices. Turning loggers off in production builds also ensures that your log statements don't cause performance problems when it matters.

You can send log messages at different levels, then you can filter out all logs below a given level. For example, sometimes you only care about errors and warnings. Other times you might care about errors, warnings, info, traces, etc. With loggers, you can configure these levels during development to dig into the problem before you.

Use the logging package

The Dart team publishes a package called logging. The logging package is a simple but useful tool for creating and using loggers. If you're not already used to logging messages in Dart and Flutter, then I recommend using the logging package for a while. Once you're in the habit of using Loggers instead of print, you can investigate more sophisticated logging packages that might offer further tools for your use-cases.

Get started with the logging package

To help you get started with the logging package, the following logger.dart file can be dropped into your codebase and used as a starting point.

// ignore_for_file: avoid_print
import 'package:logging/logging.dart';

/// Send log output from all loggers, at or above the given [level], to the terminal.
void initAllLogs(Level level) {
  initLoggers(level, {Logger.root});

/// Send output from the given [loggers], at or above the given [level], to the terminal.
void initLoggers(Level level, Set<Logger> loggers) {
  hierarchicalLoggingEnabled = true;

  for (final logger in loggers) {
    if (!_activeLoggers.contains(logger)) {
      print('Initializing logger: ${}');
        ..level = level

    } else {
      // The logger is already active. Adjust the log level as desired.
      logger.level = level;

/// Returns `true` if the given [logger] is currently logging, or
/// `false` otherwise.
/// Generally, developers should call loggers, regardless of whether
/// a given logger is active. However, sometimes you may want to log
/// information that's costly to compute. In such a case, you can
/// choose to compute the expensive information only if the given
/// logger will actually log the information.
bool isLogActive(Logger logger) {
  return _activeLoggers.contains(logger);

/// Stop the given [loggers] from sending any output to the terminal.
void deactivateLoggers(Set<Logger> loggers) {
  for (final logger in loggers) {
    if (_activeLoggers.contains(logger)) {
      print('Deactivating logger: ${}');


void _printLog(LogRecord record) {
    '(${record.time.second}.${record.time.millisecond.toString().padLeft(3, '0')}) ${record.loggerName} > ${}: ${record.message}',

final _activeLoggers = <Logger>{};

With this file added to your project, you can accomplish various logging goals.

Activate all logs at a given level:

void main() {

Activate only the logs you care about:

void main() {
    Level.INFO, {

Activate the logs only in debug mode:

void main() {
  if (kDebugMode) {

The logging package lets you instantiate Loggers wherever you'd like. You can define them all together, or separately.

// logger.dart
// Define loggers together
final appInitLog = Logger("app-init");
final authLog = Logger("auth");
final networkLog = Logger("network");

// --- or define them separately ---

// app.dart
final appInitLog = Logger("app-init");

// auth.dart
final authLog = Logger("auth");

// network_client.dart
final networkLog = Logger("network");

The logging package also supports hierarchical Loggers based on their name. This makes it easy to enable an entire set of related Loggers.

final authLog = Logger("auth");
final authCacheLog = Logger("auth.cache");
final googleAuthLog = Logger("");
final appleAuthLog = Logger("");

void main() {
  if (kDebugMode) {
    // Enabling the authLog automatically enables all sub-logs, including
    // "auth.cache", "", and "".
    initLoggers(Level.INFO, {authLog});

What to log

Choosing what to log is an art and a science. There's no universal answer. You should use your knowledge of your development process to decide where it makes sense.

First, you shouldn't convert all of your debugging print statements into logs. When you're actively debugging your code, you'll print a lot of information that's only relevant to what you're doing right now. Once you fix your bug, you should delete most of those print statements entirely. However, along the way, you might find some places in the code where the team could use logs on a regular basis.

For example, it might be a good idea to log information from each of your network calls:

class MyNetworkClient {
  Future<void> post(String url, String? body) async {
    try {"POST: $url\nBody: $body");
      // the network call...
    } catch (exception, stacktrace) {
      networkLog.warn("Failed to POST to $url with body $body");

Similarly, you might log requests, events, and results for areas like app initialization, user authentication, routing, and database communication.

Now go log stuff. Start simple with the logging package. Start humble with just a few Loggers. Then spread out as needed.

Did you find this helpful? Would you consider a monthly sponsorship so I can keep writing guides like this?