Skip to content

avoid_default_tostring

v0.8.0WarningConfigurableCode Quality

This rule flags string interpolation of a value whose class does not override toString.

Object.toString returns Instance of 'Foo' — the one thing the reader of a log already knows. It bites hardest in the error message written to diagnose a failure, or the log line captured from production, which turns out to say nothing.

This rule is in the opinionated preset and works with its defaults; no configuration is needed.

class User {
const User(this.id, this.email);
final String id;
final String email;
}
void logFailure(User user, void Function(String) log) {
// Logs: "upload failed for Instance of 'User'"
log('upload failed for $user');
}

Either give the class a toString, so every interpolation of it reads:

class User {
const User(this.id, this.email);
final String id;
final String email;
@override
String toString() => 'User(id: $id, email: $email)';
}
void logFailure(User user, void Function(String) log) {
// Logs: "upload failed for User(id: u_17, email: a@b.com)"
log('upload failed for $user');
}

…or interpolate the fields the message actually needs:

void logFailure(User user, void Function(String) log) {
log('upload failed for ${user.id}');
}

A thrown exception, interpolated into its own message

Section titled “A thrown exception, interpolated into its own message”

The most expensive version: the message is built precisely so a human can read it.

class ParseFailure implements Exception {
const ParseFailure(this.line);
final int line;
}
// Don't — the throw site says "Instance of 'ParseFailure'".
Never rethrowWithContext(ParseFailure failure) =>
throw StateError('import aborted: $failure');
class ParseFailure implements Exception {
const ParseFailure(this.line);
final int line;
@override
String toString() => 'ParseFailure(line $line)';
}
Never rethrowWithContext(ParseFailure failure) =>
throw StateError('import aborted: $failure');

The check walks the whole supertype chain, so a base class that overrides toString covers its subclasses:

abstract class Failure {
const Failure(this.code);
final String code;
@override
String toString() => '$runtimeType($code)';
}
class TimeoutFailure extends Failure {
const TimeoutFailure() : super('timeout');
}
// Not reported — TimeoutFailure inherits Failure.toString.
String describe(TimeoutFailure failure) => 'failed: $failure';

Status.active already reads well, so enums are skipped by default. Turn report_enums: true on if you want them spelled out:

many_lints.yaml
rules:
avoid_default_tostring:
report_enums: true
enum Status { active, archived }
// Reported only with `report_enums: true`.
String label(Status status) => 'status: $status';

Only classes declared in the analysed code are reported. An SDK type without a toString override is not the user’s to fix, so it is skipped.

Enums, records, and core types render usefully by default and are never reported. report_enums: true widens the rule to enums only.

analysis_options.yaml
many_lints:
rules:
avoid_default_tostring:
report_enums: true
Option Type Default Description
report_enums bool false Also report interpolated enums without a toString override. Off by default because Status.active already reads well

This rule is in the opinionated preset, so it is on with preset: opinionated, or by name:

many_lints.yaml
rules:
avoid_default_tostring: true

To turn it off again:

many_lints.yaml
rules:
avoid_default_tostring: false

To keep the rule on but skip certain paths, use per-rule exclude.