Watch the video (10 min)
On this page25 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What Is a Lint?
  5. 2. Where the Rules Come From
  6. 3. Core vs Recommended
  7. 4. Enabling Extra Rules
  8. 5. Two Rules That Catch Real Bugs
  9. 6. Turning a Rule Off
  10. 7. Changing a Rule's Severity
  11. 8. Effective Dart: Four Guides
  12. 9. Style — Naming
  13. 10. Documentation
  14. 11. Usage
  15. 12. Design
  16. 13. A Real-World Example
  17. Code Examples
  18. Common Beginner Mistakes
  19. Best Practices
  20. Mini Challenge
  21. Practice Questions
  22. Practice Answers
  23. Quick Revision
  24. Key Takeaways
  25. What's Next?

Lecture 117 — Linting & Effective Dart

Category19 — Dart Developer Tools
Lecture117 of 140
Estimated video length10 min
TargetDart 3.12.2
LevelAdvanced

A program can compile, run and print the right answer — and still be badly named, hard to read, or one forgotten await away from a bug. Lint rules catch those problems, and Effective Dart explains the style they enforce.

Contents

Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways

Every message in this lecture is real. Each one was produced by running dart run and dart analyze on the code shown, on Dart 3.12.2. Every program lives in bin/main.dart of a project made with dart create.


Prerequisites

Students should already understand, from Lectures 001–116:

  • analysis_options.yaml and dev_dependencies in a project (Lectures 002, 104)
  • running dart analyze and reading error, warning and info lines (Lecture 114)
  • naming variables, and final (Lectures 004, 006); dynamic (Lecture 011)
  • exceptions versus errors (Lectures 083–084), and await (Lecture 089)

Connection With Previous Lectures

In Lecture 114, dart analyze printed lines ending in a name like - non_constant_identifier_names. Lecture 084 promised the optional rule avoid_catching_errors; Lecture 089 switched on unawaited_futures. But where do these rules come from, who chose them, and how do you choose your own?


Learning Objectives

By the end of this lecture you will be able to:

  • Explain what a lint is and how it differs from a compile error
  • Distinguish the core and recommended rule sets from package:lints
  • Enable extra rules in analysis_options.yaml
  • Disable a rule for a project, a file or a single line
  • Change a rule's severity with analyzer: errors:
  • Name Effective Dart's four guides: Style, Documentation, Usage and Design
  • Apply the Dart naming rules for types, variables and files
  • Write a program that passes dart analyze with a strict rule set

1. What Is a Lint?

A lint is a warning about code that is legal but probably wrong, unclear, or against the common style. Each check is a lint rule with a snake_case name.

Analogy — a word processor underlines broken spelling in red and grammar suggestions in blue. Compile errors are the red lines; lints are the blue ones.

A lint never stops dart run; it appears in dart analyze and your editor.


2. Where the Rules Come From

dart create puts two things in every project. In pubspec.yaml, a dev dependency (Lecture 104):

yaml
dev_dependencies:
  lints: ^6.0.0

And in analysis_options.yaml, one line that switches on that package's rules:

yaml
include: package:lints/recommended.yaml

This program runs fine:

dart
const MAX_MARKS = 100;

void main() {
  var Total_marks = 80 + 75 + 85;
  print('Total: ' + Total_marks.toString() + ' of ${MAX_MARKS * 3}');
}
output
Total: 240 of 300

But dart analyze finds three problems:

output
   info - bin/main.dart:1:7 - The constant name 'MAX_MARKS' isn't a lowerCamelCase identifier. Try changing the name to follow the lowerCamelCase style. - constant_identifier_names
   info - bin/main.dart:4:7 - The variable name 'Total_marks' isn't a lowerCamelCase identifier. Try changing the name to follow the lowerCamelCase style. - non_constant_identifier_names
   info - bin/main.dart:5:9 - Use interpolation to compose strings and values. Try using string interpolation to build the composite string. - prefer_interpolation_to_compose_strings

3 issues found.

The last word of each line is the rule's name.


package:lints ships two sets:

SetSizeContains
core.yamlabout 35 rulesthe most critical checks; pub.dev uses them to score packages
recommended.yamlcore + about 60 moreadds style and readability checks

Switch the same project to the smaller set:

yaml
include: package:lints/core.yaml
output
   info - bin/main.dart:4:7 - The variable name 'Total_marks' isn't a lowerCamelCase identifier. Try changing the name to follow the lowerCamelCase style. - non_constant_identifier_names

1 issue found.

Two lints vanished: they belong to recommended only. Keep recommended.


4. Enabling Extra Rules

The linter knows far more rules than either set includes. Add your own under linter: → rules:, one per line:

yaml
include: package:lints/recommended.yaml

linter:
  rules:
    - prefer_final_locals
    - always_declare_return_types
    - avoid_catching_errors
    - unawaited_futures

This program forgets a return type and never changes marks:

dart
average(List<int> marks) {
  var total = 0;
  for (final mark in marks) {
    total += mark;
  }
  return total / marks.length;
}

void main() {
  var marks = [80, 75, 85];
  print('Average: ${average(marks)}');
}
output
   info - bin/main.dart:1:1 - Missing type annotation. Try adding a type annotation. - strict_top_level_inference
   info - bin/main.dart:1:1 - The function 'average' should have a return type but doesn't. Try adding a return type to the function. - always_declare_return_types
   info - bin/main.dart:10:3 - Local variables should be final. Try making the variable final. - prefer_final_locals

3 issues found.

total is not flagged — it changes inside the loop. Write double average(...) and final marks = ...:

output
No issues found!

5. Two Rules That Catch Real Bugs

With the same configuration:

dart
Future<void> saveMarks(String name) async {
  await Future.delayed(Duration(milliseconds: 100));
  print('Saved marks for $name');
}

Future<void> main() async {
  final marks = [80, 75, 85];
  try {
    print(marks[3]);
  } on RangeError {
    print('No fourth mark');
  }
  saveMarks('Ali');
  print('Done');
}
output
No fourth mark
Done
Saved marks for Ali
output
   info - bin/main.dart:10:5 - The type 'RangeError' should not be caught because it is a subclass of 'Error'. Try removing the catch or catching an 'Exception' instead. - avoid_catching_errors
   info - bin/main.dart:13:3 - Missing an 'await' for the 'Future' computed by this expression. Try adding an 'await' or wrapping the expression with 'unawaited'. - unawaited_futures

2 issues found.

marks[3] is a bug to fix, not catch (Lecture 084), and Done printed before the save finished (Lecture 089).


6. Turning a Rule Off

To switch off a rule — even one from recommended — write the rules as a map with true or false:

yaml
include: package:lints/recommended.yaml

linter:
  rules:
    prefer_final_locals: true
    prefer_interpolation_to_compose_strings: false

Use one form or the other: a list of - names, or a map of name: true/false. Never mix them (see Common Mistakes).

To silence a rule for one line, put // ignore: rule_name on the line above:

dart
void main() {
  final marks = [80, 75, 85];
  try {
    print(marks[3]);
    // ignore: avoid_catching_errors
  } on RangeError {
    print('No fourth mark');
  }
}
output
No issues found!

// ignore_for_file: rule_name at the top silences the whole file. Use both rarely.


7. Changing a Rule's Severity

Every lint reports as info. The analyzer: → errors: section changes that to warning, error, or ignore:

yaml
include: package:lints/recommended.yaml

analyzer:
  errors:
    non_constant_identifier_names: error
    constant_identifier_names: warning
    prefer_interpolation_to_compose_strings: ignore

On the program from section 2:

output
  error - bin/main.dart:4:7 - The variable name 'Total_marks' isn't a lowerCamelCase identifier. Try changing the name to follow the lowerCamelCase style. - non_constant_identifier_names
warning - bin/main.dart:1:7 - The constant name 'MAX_MARKS' isn't a lowerCamelCase identifier. Try changing the name to follow the lowerCamelCase style. - constant_identifier_names

2 issues found.

dart run still runs the program. But dart analyze now fails — with only info lines it succeeds. Teams use this to block code that breaks their key rules.


8. Effective Dart: Four Guides

The rules follow Effective Dart, the official style guide on dart.dev. It has four parts:

GuideAnswers
StyleHow should code look? Names, formatting, order of imports.
DocumentationHow do I write comments and doc comments?
UsageWhich language feature fits best here?
DesignHow should my classes and functions look to people who use them?

Each guideline starts with DO, DON'T, PREFER, AVOID or CONSIDER; many have a matching lint.


9. Style — Naming

Kind of nameStyleExamples
classes, enums, typedefs, extensionsUpperCamelCaseStudentReport, OrderStatus
variables, functions, parameters, constantslowerCamelCasetotalMarks, maxMarks
files, packages, import prefixeslowercase_with_underscoresstudent_report.dart, as math

DON'T write constants as MAX_MARKS — Dart uses maxMarks. Breaking the other two:

dart
class student_report {}

void main() {}
output
   info - bin/main.dart:1:7 - The type name 'student_report' isn't an UpperCamelCase identifier. Try changing the name to follow the UpperCamelCase style. - camel_case_types

A file named Student_Report.dart gets a file_names lint. Formatting is part of Style too — dart format (Lecture 114) handles it.


10. Documentation

DO use /// for doc comments (Lecture 003), DO start with a one-sentence summary, and DO put names from your code in [square brackets]. The old /** */ form is flagged:

dart
/** Returns the average of [marks]. */
double average(List<int> marks) {
  var total = 0;
  for (final mark in marks) {
    total += mark;
  }
  return total / marks.length;
}

void main() {
  print(average([80, 75, 85]));
}
output
80.0
output
   info - bin/main.dart:1:1 - Use the end-of-line form ('///') for doc comments. Try rewriting the comment to use '///'. - slash_for_doc_comments

Write /// Returns the average of [marks]. instead. Use // for notes that explain why.


11. Usage

Three Usage guidelines, all in recommended:

  • DON'T use .length == 0 to test emptiness — use isEmpty / isNotEmpty.
  • DON'T initialize a variable to null — nullable variables start as null anyway.
  • PREFER interpolation over + to build strings.
dart
void main() {
  List<int> marks = [];
  String? teacher = null;
  if (marks.length == 0) {
    print('No marks for ' + (teacher ?? 'Sara'));
  }
}
output
No marks for Sara
output
   info - bin/main.dart:3:11 - Redundant initialization to 'null'. Try removing the initializer. - avoid_init_to_null
   info - bin/main.dart:4:7 - Use 'isEmpty' instead of 'length' to test whether the collection is empty. Try rewriting the expression to use 'isEmpty'. - prefer_is_empty
   info - bin/main.dart:5:11 - Use interpolation to compose strings and values. Try using string interpolation to build the composite string. - prefer_interpolation_to_compose_strings

3 issues found.

Fixed — same output, no issues:

dart
void main() {
  final marks = <int>[];
  String? teacher;
  if (marks.isEmpty) {
    print('No marks for ${teacher ?? 'Sara'}');
  }
}

Usage also says: be consistent with var or final for locals. prefer_final_locals enforces a team's choice of final.


12. Design

Design guidelines shape what other code sees: DO name classes with nouns and methods with verbs, PREFER positive names for booleans (isPassed, not isNotFailed), DO declare return types, and AVOID dynamic unless you really want to switch off type checks. Here is why:

dart
class StudentReport {
  final List<int> marks;
  StudentReport(this.marks);
}

void main() {
  dynamic report = StudentReport([80, 75]);
  print(report.mark);
}
output
Unhandled exception:
NoSuchMethodError: Class 'StudentReport' has no instance getter 'mark'.

The typo mark compiled and crashed at runtime. The optional rule avoid_dynamic_calls flags that line before you run it (excerpt):

output
   info - bin/main.dart:8:9 - Method invocation or property access on a 'dynamic' target. Try giving the target a type. - avoid_dynamic_calls

With final report = ..., Dart knows the type and the typo becomes a compile error: The getter 'mark' isn't defined for the type 'StudentReport'.


13. A Real-World Example

Sara's team uses this analysis_options.yaml:

yaml
include: package:lints/recommended.yaml

analyzer:
  errors:
    unawaited_futures: warning

linter:
  rules:
    - prefer_final_locals
    - always_declare_return_types
    - avoid_catching_errors
    - unawaited_futures
    - avoid_dynamic_calls

And code that follows all four guides:

dart
/// One student's exam result.
class StudentReport {
  /// Creates a report for [name] with their [marks].
  StudentReport(this.name, this.marks);

  final String name;
  final List<int> marks;

  /// The average of all marks, or 0 when there are none.
  double get average {
    if (marks.isEmpty) {
      return 0;
    }
    return marks.reduce((a, b) => a + b) / marks.length;
  }

  /// Whether the average is 50 or more.
  bool get isPassed => average >= 50;
}

Future<void> saveReport(StudentReport report) async {
  await Future.delayed(Duration(milliseconds: 100));
  print('Saved report for ${report.name}');
}

Future<void> main() async {
  final reports = [
    StudentReport('Ali', [80, 75, 85]),
    StudentReport('Zayan', [40, 35, 50]),
  ];
  for (final report in reports) {
    final result = report.isPassed ? 'passed' : 'failed';
    print('${report.name}: ${report.average.toStringAsFixed(1)} ($result)');
    await saveReport(report);
  }
}
output
Ali: 80.0 (passed)
Saved report for Ali
Zayan: 41.7 (failed)
Saved report for Zayan
output
No issues found!

All four guides at work: Style names, /// summaries, isEmpty and interpolation (Usage), typed values and a positive isPassed (Design).


Code Examples

Every example passes dart analyze with the configuration from section 13.

Example 1 — Naming Styles

dart
const maxMarks = 100;

class ExamResult {
  final int score;
  ExamResult(this.score);
}

void main() {
  final aliResult = ExamResult(85);
  print('${aliResult.score} / $maxMarks');
}
output
85 / 100

Example 2 — An Import Prefix

dart
import 'dart:math' as math;

void main() {
  print(math.max(80, 95));
}
output
95

Example 3 — final Unless It Changes

dart
void main() {
  final marks = [70, 90, 80];
  var best = 0;
  for (final mark in marks) {
    if (mark > best) {
      best = mark;
    }
  }
  print('Best: $best');
}
output
Best: 90

Example 4 — A Declared Return Type

dart
int totalMarks(List<int> marks) {
  var total = 0;
  for (final mark in marks) {
    total += mark;
  }
  return total;
}

void main() {
  print(totalMarks([80, 75, 85]));
}
output
240

Example 5 — A Doc Comment

dart
/// Returns [price] reduced by [percent] percent.
double discounted(double price, double percent) {
  return price - price * percent / 100;
}

void main() {
  print(discounted(1500, 20));
}
output
1200.0

Example 6 — isEmpty and isNotEmpty

dart
void main() {
  final cart = <String>[];
  print(cart.isEmpty);
  cart.add('Notebook');
  print(cart.isNotEmpty);
}
output
true
true

Example 7 — Student Marks

dart
void main() {
  const passMark = 50;
  final marks = {'Ali': 80, 'Sara': 92, 'Zayan': 45};
  marks.forEach((name, mark) {
    final result = mark >= passMark ? 'pass' : 'fail';
    print('$name: $mark ($result)');
  });
}
output
Ali: 80 (pass)
Sara: 92 (pass)
Zayan: 45 (fail)

Example 8 — Shopping Total Without dynamic

dart
void main() {
  final Map<String, double> prices = {'Pen': 50, 'Bag': 1500};
  var total = 0.0;
  for (final price in prices.values) {
    total += price;
  }
  print('Total: $total');
}
output
Total: 1550.0

Example 9 — Bank Balance: Catch Exceptions, Not Errors

dart
void main() {
  var balance = 1000.0;
  try {
    balance += double.parse('250x');
  } on FormatException {
    print('Invalid amount');
  }
  print('Balance: $balance');
}
output
Invalid amount
Balance: 1000.0

Example 10 — Temperature Log with await

dart
Future<void> logReading(double celsius) async {
  await Future.delayed(Duration(milliseconds: 100));
  print('Logged $celsius°C');
}

Future<void> main() async {
  await logReading(36.6);
  print('All readings saved');
}
output
Logged 36.6°C
All readings saved

Common Beginner Mistakes

1. Mixing the list and map forms

yaml
include: package:lints/recommended.yaml

linter:
  rules:
    - prefer_final_locals
    prefer_interpolation_to_compose_strings: false # ✗ mixed with "-" entries
output
  error - analysis_options.yaml:6:5 - While parsing a block collection, expected '-'. - parse_error

2. Misspelling a rule name

yaml
include: package:lints/recommended.yaml

linter:
  rules:
    - prefer_final_local # ✗ missing "s"
output
warning - analysis_options.yaml:5:7 - 'prefer_final_local' isn't a recognized lint rule. Try using the name of a recognized lint rule. - undefined_lint

3. Enabling rules that contradict each other

unnecessary_final wants var where prefer_final_locals wants final:

output
warning - analysis_options.yaml:6:7 - The rule 'unnecessary_final' is incompatible with ''prefer_final_locals''. Try removing all but one of the incompatible rules. - incompatible_lint

Pick one style per project.

4. Expecting lints to stop dart run

Even a lint raised to error only affects dart analyze.


Best Practices

  • Keep include: package:lints/recommended.yaml in every project.
  • Add the extra rules your team agrees on, and raise bug-catching ones to warning.
  • Run dart format and dart analyze before every commit; aim for No issues found!.
  • Fix rather than // ignore: — and ignore one line, never the whole file.
  • Give public classes and functions a /// one-sentence summary.

Mini Challenge

With the configuration from section 13, this program prints the right answer but has lint issues. Make dart analyze report No issues found! without changing the output:

dart
const START_AMOUNT = 150;

piggyBank(int spent) {
  var Balance = START_AMOUNT + 50 - spent;
  return 'Zayan has ' + Balance.toString() + ' rupees';
}

void main() {
  print(piggyBank(30));
}
output
Zayan has 170 rupees

Try it yourself before opening the solution.

Show solution
dart
const startAmount = 150;

/// Returns Zayan's balance after spending [spent] rupees.
String piggyBank(int spent) {
  final balance = startAmount + 50 - spent;
  return 'Zayan has $balance rupees';
}

void main() {
  print(piggyBank(30));
}
output
Zayan has 170 rupees
output
No issues found!

Practice Questions

They run from beginner to application to challenge. Use the configuration from section 13.

Beginner

  1. Which file configures lint rules, and which line turns on the recommended set?
  2. Give the correct Dart name for: a class "bank account", a variable "total marks", a constant "max marks", and a file "shopping cart".
  3. A program has three info lints. Does dart run still run it? Does dart analyze succeed?

Application

  1. Write a linter: section that enables prefer_final_locals and avoid_dynamic_calls.

  2. Write a linter: section that keeps prefer_final_locals on and turns prefer_interpolation_to_compose_strings off.

  3. Write an analyzer: section that makes non_constant_identifier_names an error and ignores constant_identifier_names.

  4. Fix this program so it has no issues, and keep the output Cart is empty:

    dart
    void main() {
      List<String> cart = [];
      if (cart.length == 0) {
        print('Cart is ' + 'empty');
      }
    }
  5. Which lint reports the on StateError line below, and what should you write instead?

    dart
    void main() {
      final names = ['Ali', 'Sara'];
      try {
        print(names.firstWhere((n) => n == 'Zayan'));
      } on StateError {
        print('Not found');
      }
    }

Challenge

  1. Write a documented function celsiusToFahrenheit with a declared return type, and print the result for 25. The output must be 77.0, with no issues.

  2. Rewrite this program without dynamic so that the typo is caught before it runs. Then fix the typo and print Balance: 2000.0.

    dart
    class Account {
      double balance = 2000;
    }
    
    void main() {
      dynamic account = Account();
      print('Balance: ${account.balanse}');
    }

Practice Answers

Question 1 — Solution

analysis_options.yaml, with the line:

yaml
include: package:lints/recommended.yaml

Question 2 — Solution

BankAccount, totalMarks, maxMarks, and shopping_cart.dart.

Question 3 — Solution

Yes, it runs. dart analyze lists three issues but succeeds — they are only info.

Question 4 — Solution

yaml
linter:
  rules:
    - prefer_final_locals
    - avoid_dynamic_calls

Question 5 — Solution

yaml
linter:
  rules:
    prefer_final_locals: true
    prefer_interpolation_to_compose_strings: false

Question 6 — Solution

yaml
analyzer:
  errors:
    non_constant_identifier_names: error
    constant_identifier_names: ignore

Question 7 — Solution

dart
void main() {
  final cart = <String>[];
  if (cart.isEmpty) {
    print('Cart is empty');
  }
}
output
Cart is empty
output
No issues found!

Question 8 — Solution

avoid_catching_errors — StateError is an Error. Fix the code instead with orElse (Lecture 042):

dart
void main() {
  final names = ['Ali', 'Sara'];
  print(names.firstWhere((n) => n == 'Zayan', orElse: () => 'Not found'));
}
output
Not found

Question 9 — Solution

dart
/// Converts [celsius] to degrees Fahrenheit.
double celsiusToFahrenheit(double celsius) {
  return celsius * 9 / 5 + 32;
}

void main() {
  print(celsiusToFahrenheit(25));
}
output
77.0

Question 10 — Solution

With final account = Account();, the typo becomes a compile error: The getter 'balanse' isn't defined for the type 'Account'. Fixed:

dart
class Account {
  double balance = 2000;
}

void main() {
  final account = Account();
  print('Balance: ${account.balance}');
}
output
Balance: 2000.0

Quick Revision

  1. What is the difference between a lint and a compile error?
  2. Under which key do you add extra lint rules?
  3. How do you turn off one rule for the whole project? For one line?
  4. Which section changes a lint's severity, and what are the four values?
  5. Name Effective Dart's four guides.

Key Takeaways

  • A lint flags legal code that is risky or off-style; it never stops dart run.
  • package:lints offers core (critical) and recommended (core + style, the default).
  • Add rules under linter: rules:, as a list or as a true/false map — never both.
  • analyzer: errors: sets a rule to info, warning, error or ignore; warnings and errors make dart analyze fail.
  • Effective Dart has four guides: Style, Documentation, Usage, Design.
  • Types are UpperCamelCase, members and constants lowerCamelCase, files lowercase_with_underscores.
  • Avoid dynamic: typed code turns runtime crashes into compile errors.

What's Next?

Lecture 118 — Introduction to Testing. That closes Category 19. But No issues found! only means the code is clean — not that it gives the right answers. Next we learn how automated tests prove that it does.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.