LogoDart Code Generation

Debugging a Dart Build Generator#

Ten concrete techniques for diagnosing failures in build_runner / source_gen generators.


1. Run with verbose logging#

The single most useful first step. Verbose mode prints every builder invocation, asset resolution, and cache decision.

dart run build_runner build --verbose

For watch mode:

dart run build_runner watch --verbose --delete-conflicting-outputs

Look for lines prefixed with the builder name to see exactly what inputs it received and what output it tried to write.


2. Delete the build cache#

Stale cache entries cause phantom failures — the builder never reruns because build_runner thinks nothing changed.

dart run build_runner clean
dart run build_runner build

Do this whenever:

  • Output is missing but no error is shown
  • Old generated code persists after you changed the generator
  • You renamed or moved files

3. Attach a real Dart debugger#

build_runner spawns a child process to run the build. Debugger flags must be passed through to that child process via --dart-jit-vm-arg — you cannot attach to the parent dart run build_runner process directly.

Start the build with the VM service enabled#

dart run build_runner build \
  --dart-jit-vm-arg=--observe \
  --dart-jit-vm-arg=--pause-isolates-on-start
  • --observe starts the Dart VM service (defaults to port 8181)
  • --pause-isolates-on-start holds the child process until a debugger attaches

The child process prints a DevTools URL:

The Dart DevTools debugger and profiler is available at:
http://127.0.0.1:8181/3xXtAPE8msc=/devtools/?uri=ws://127.0.0.1:8181/3xXtAPE8msc=/ws

Attach from your IDE#

VS Code: open the Command Palette → Debug: Attach to Dart Process → paste the URL above.

IntelliJ / Android Studio: Run → Attach to Process → paste the VM service URL.

Dart DevTools (browser): open the devtools/ URL directly in Chrome.

Set breakpoints before attaching#

Place breakpoints in your generator's generate() or generateForAnnotatedElement() method before attaching. Because --pause-isolates-on-start holds execution, the process waits until you resume from the debugger.

Custom VM service port#

If port 8181 is in use:

dart run build_runner build \
  --dart-jit-vm-arg=--observe=8182

watch mode#

Works the same way — use watch instead of build. The debugger session stays alive across rebuilds.

dart run build_runner watch \
  --dart-jit-vm-arg=--observe \
  --dart-jit-vm-arg=--pause-isolates-on-start

4. Use log instead of print in production, capture in tests#

The build package provides a zone-scoped logger. It surfaces warnings/errors even without --verbose and is capturable in tests.

import 'package:build/build.dart';

// Inside build() or generate():
log.info('Processing ${buildStep.inputId}');
log.warning('Abstract class skipped: ${element.name}');
log.severe('Unexpected element kind: ${element.kind}');

Capture in tests:

final logs = <LogRecord>[];
await testBuilder(
  myBuilder(BuilderOptions.empty),
  {'pkg|lib/model.dart': source},
  onLog: logs.add,
);

expect(logs.map((r) => r.message), contains(contains('skipped')));

5. Inspect the resolved element tree in the debugger#

Unexpected generation output usually means you misread the AST/element model. With the debugger attached (step 3), set a breakpoint at the top of generate() or generateForAnnotatedElement() and inspect the live objects.

Useful watch expressions:

ExpressionWhat to look for
library.allElements.toList() All top-level elements the analyzer resolved
element.runtimeType Confirm it's ClassElementImpl, not a typedef or extension
element.fields All fields including synthetic getters — filter with !f.isSynthetic
element.constructors Check the unnamed constructor exists and has the expected params
element.typeParametersNon-empty for generic classes
element.supertypeThe resolved superclass (may be Object)

Set a conditional breakpoint (e.g., element.name == 'MyModel') to stop only on the class you care about.


6. Inspect annotation values in the debugger#

If your generator reads annotation parameters and produces wrong output, set a breakpoint after annotated.annotation is in scope and inspect the ConstantReader directly.

Useful watch expressions on reader (ConstantReader):

ExpressionWhat to look for
reader.objectValue The raw DartObject — expand to see all fields
reader.objectValue.fields Map of field name → DartObject value
reader.peek('name') null if the field was not set (uses default)
reader.peek('name')?.stringValue The actual string, or throws if not a string
reader.peek('mode')?.revive().accessorEnum constant name as a string

Common pitfalls (visible once you can inspect live values):

  • read('field') throws if the field is absent — use peek('field') to get null instead
  • Enum values: revive().accessor gives the constant name; match against MyEnum.values
  • Nested annotations: ConstantReader(reader.read('nested').objectValue) to recurse

7. Use testBuilder for fast isolated reproduction#

Running the full build_runner is slow and caches state. Reproduce the failing case in a unit test for a fast feedback loop.

import 'package:build_test/build_test.dart';
import 'package:test/test.dart';

void main() {
  test('reproduces the bug', () async {
    await testBuilder(
      myBuilder(BuilderOptions.empty),
      {
        'pkg|lib/model.dart': '''
          import 'package:my_annotations/my_annotations.dart';
          part 'model.g.dart';

          @MyAnnotation(name: 'custom')
          class Model {
            final String id;
          }
        ''',
      },
      outputs: {
        'pkg|lib/model.my_builder.g.part': decodedMatches(
          contains('custom'),
        ),
      },
    );
  });
}

Run with: dart test test/my_repro_test.dart


8. Check build.yaml configuration issues#

Most "my builder doesn't run" problems come from a misconfigured build.yaml.

builders:
  my_builder:
    import: "package:my_generator/builder.dart"
    builder_factories: ["myBuilder"]           # must match function name exactly
    build_extensions: {".dart": [".my_builder.g.part"]}
    auto_apply: dependents                     # runs for packages that depend on this one
    build_to: cache                            # use 'source' only for standalone file builders
    applies_builders: ["source_gen|combining_builder"]  # required for SharedPartBuilder

Common mistakes:

SymptomLikely cause
Builder never runsauto_apply: none or wrong package dependency
Output file not createdbuild_to: cache but checking source directory
combining_builder output missingapplies_builders not set
Factory not foundbuilder_factories name mismatch with actual function

Verify with: dart run build_runner build --verbose 2>&1 | grep -i "my_builder"


9. Handle InvalidGenerationSourceError with precise messages#

When the generator throws without a message, build_runner prints a generic stack trace. Throw InvalidGenerationSourceError with context so the error is actionable.

import 'package:source_gen/source_gen.dart';

// Bad — produces an unhelpful stack trace
if (element is! ClassElement) throw Exception('not a class');

// Good — points to the offending element in the source file
if (element is! ClassElement) {
  throw InvalidGenerationSourceError(
    '@MyAnnotation can only be applied to classes. '
    'Found: ${element.runtimeType} "${element.name}".',
    element: element,   // IDE and build output will link to this location
  );
}

Test that errors are thrown correctly:

test('throws on enum target', () {
  expect(
    () => testBuilder(
      myBuilder(BuilderOptions.empty),
      {'pkg|lib/bad.dart': '@MyAnnotation()\nenum Bad { a }'},
    ),
    throwsA(
      isA<InvalidGenerationSourceError>().having(
        (e) => e.message, 'message', contains('classes'),
      ),
    ),
  );
});

10. Verify the generated output parses as valid Dart#

A generator that produces syntactically invalid Dart causes silent failures during the combining step — the .g.dart file is never written or contains a syntax error that's hard to trace back to your generator.

import 'package:dart_style/dart_style.dart';

// Inside generate(), before returning:
String? _formatAndValidate(String code) {
  final formatter = DartFormatter();
  try {
    return formatter.format(code);
  } on FormatterException catch (e) {
    log.severe('Generated code is not valid Dart:\n$e\n\n--- code ---\n$code');
    rethrow;
  }
}

In tests, assert the output is parseable:

import 'package:analyzer/dart/analysis/utilities.dart';

test('output is valid Dart', () async {
  final writer = RecordingAssetWriter();
  await testBuilder(myBuilder(BuilderOptions.empty), inputs, writer: writer);

  final outputId = AssetId('pkg', 'lib/model.my_builder.g.part');
  final content = utf8.decode(writer.assets[outputId]!);

  // parseString throws ParseStringResult with errors if invalid
  final result = parseString(content: content);
  expect(result.errors, isEmpty, reason: 'Generated code has parse errors');
});