skip to content

A Dart CSV importer must report every bad row without aborting the whole file; how would you structure its exception handling?

level: seniorimportance: should knowfreq 30%

answer

  1. row boundary versus file boundary
  2. validate before you parse
  3. tryParse over catching FormatException
  4. RowException with row and column
  5. Errors still crash the import

basics

~20 s

In a Dart importer, give each row its own try with on RowException, record row, column and message in a report and continue; let file-level exceptions end the import, and never catch Error, which means the importer is buggy.

solid answer

~50 s

I split the failures by scope. A bad row is expected input, so each row gets its own `try` with `on RowException catch (e)`, and the handler appends the row, column and message to a report and moves on. Inside the row parser I validate before I parse — check `cells.length` rather than letting `cells[2]` throw a `RangeError`, and use `int.tryParse` so a bad number becomes a `RowException` without a throw-and-catch. Where a parser such as `DateTime.parse` throws `FormatException`, I translate it into a `RowException` with the column name. A missing or unreadable file is a `FileSystemException` at the file boundary: it aborts the import and propagates with its trace, cleaned up in `finally`. I never catch `Error` in the loop; a `StateError` or `TypeError` there is my bug, and hiding it would silently mis-import data.

code

dart · 54 lines
dart
import 'dart:io';

class RowException implements Exception {
  const RowException(this.row, this.column, this.message);
  final int row;
  final String column;
  final String message;

  @override
  String toString() => 'row $row, $column: $message';
}

class Item {
  const Item(this.name, this.quantity, this.delivered);
  final String name;
  final int quantity;
  final DateTime delivered;
}

class ImportReport {
  final items = <Item>[];
  final issues = <RowException>[];
}

Item parseRow(int row, String line) {
  final cells = line.split(',');
  if (cells.length != 3) {
    throw RowException(row, '*', 'expected 3 cells, got ${cells.length}');
  }
  final quantity = int.tryParse(cells[1].trim());
  if (quantity == null) {
    throw RowException(row, 'quantity', 'not an integer: "${cells[1]}"');
  }
  final DateTime delivered;
  try {
    delivered = DateTime.parse(cells[2].trim());
  } on FormatException catch (e, st) {
    Error.throwWithStackTrace(RowException(row, 'delivered', e.message), st);
  }
  return Item(cells[0].trim(), quantity, delivered);
}

ImportReport importCsv(File file) {
  final report = ImportReport();
  final lines = file.readAsLinesSync(); // file-level failure: propagates
  for (var i = 1; i < lines.length; i++) {
    try {
      report.items.add(parseRow(i + 1, lines[i]));
    } on RowException catch (e) {
      report.issues.add(e); // row-level failure: recorded, loop continues
    }
  }
  return report;
}

go deeper

for a junior

Recall that each row needs its own try with an on clause for the row failure, so one bad row does not stop the rest.

for a middle

Explain validating with tryParse and length checks, translating FormatException into one domain exception, and why the file read sits outside the row try.

for a senior

Show the policy by scope: row failures are reported, file failures abort with their trace, Errors are never caught, and translation keeps the cause.

for a principal

Weigh partial import against all-or-nothing, issue caps against complete reports, and who sees traces versus user-facing row messages.

## Two boundaries, two policies An importer meets failures at two different scopes, and the **scope decides the policy**. Mixing them up gives either an importer that dies on row 3 of 10,000 or one that reports "0 rows imported, no errors" after swallowing a bug. | Failure | Typical type | Scope | Policy | |---|---|---|---| | wrong cell count, bad number, bad date | your `RowException`, or `FormatException` translated into one | one row | record in the report, continue with the next row | | file missing or unreadable | `FileSystemException` | whole file | abort, propagate to the caller with the trace | | indexing past a row's end, a failed cast, a failed `assert` | `RangeError`, `TypeError`, `AssertionError` | the importer's code | do not catch; fix the code | ## Turn bad input into exceptions, not errors A short row that makes `cells[2]` throw `RangeError` is **bad input surfacing as a bug**, because the importer skipped a check it could have made. Catching `RangeError` would work today and hide the next real indexing bug tomorrow. So the row parser **validates first**: 1. Split the line and check `cells.length` against the header. 2. Parse numbers with `int.tryParse` / `double.tryParse`, which return `null` instead of throwing, and turn `null` into a `RowException` naming the column. 3. Where only a throwing parser exists — `DateTime.parse` throws `FormatException` — catch **that type only** and translate it into a `RowException` so the loop has one type to handle. The result is a single **domain exception** — `RowException implements Exception`, with `row`, `column` and `message` fields — that means exactly "this row is bad". ## The row loop ```dart ImportReport importCsv(File file) { final report = ImportReport(); final lines = file.readAsLinesSync(); // FileSystemException propagates for (var i = 1; i < lines.length; i++) { try { report.items.add(parseRow(i + 1, lines[i])); } on RowException catch (e) { report.issues.add(e); } } return report; } ``` Notes on the shape: - The `try` wraps **one row**, so one failure cannot skip its neighbours. - The clause is **`on RowException`**, not a bare `catch`, so a bug in `parseRow` still stops the import with a trace pointing at it. - The file read sits **outside** the loop's `try`, so a file-level failure is not mistaken for a row failure. - If the importer holds a resource — an open `RandomAccessFile`, a database transaction — release it in a `finally` around the whole import. ## Keeping context when you translate When you convert a `FormatException` from `DateTime.parse` into a `RowException`, keep the evidence. Either store the original as a `cause` field, or throw the new exception with `Error.throwWithStackTrace(rowException, st)` using the `st` from `catch (e, st)`, so a developer investigating a surprising report can still see where parsing failed. For the report shown to users, the row, column and message are what matter; the trace belongs in the developer log. ## Design choices a reviewer will probe - **Cap the issue list?** A file with every row broken — often the wrong delimiter — should stop early with one clear file-level message instead of 10,000 row reports. - **All or nothing?** Some imports must reject the whole file if any row fails; then the loop still collects every issue, but the caller commits nothing unless `issues` is empty. - **Where do Errors go?** Nowhere inside the importer. They reach the top-level reporting the app already has, which is owned by the app's own error-handling setup. - **What about exceptions nobody expected?** An `on Exception` clause at the file boundary can log and wrap them; a bare `catch` in the row loop should not exist.

  • In a Dart importer, why prefer int.tryParse over int.parse inside try with on FormatException?
    Bad numbers are the expected case in an importer, and `int.tryParse` reports them as `null` without building and unwinding an exception. It keeps the parser's control flow plain, and the SDK's own `int.parse` docs point to `tryParse` instead of throwing and immediately catching.
  • In a Dart importer, how would you handle a file where every row fails because the delimiter is wrong?
    Detect it early — for example the header has one cell, or the first N rows all fail on cell count — and abort with one file-level exception explaining the likely cause. Ten thousand identical row reports bury the real problem and waste the user's time.
  • In a Dart importer, why does the per-row try use on RowException rather than on Exception?
    Only `RowException` means "this row is bad, keep going". A broader `on Exception` would also treat an unexpected `FileSystemException` or a library's own exception as a bad row, reporting it to the user as input trouble instead of stopping the import.

saying these in an interview costs you the question

  • Wrap the whole loop in one catch (e) so nothing can crash the import.
  • Catch RangeError to skip rows that have too few cells.
  • A missing file should be reported as a bad first row and skipped.
  • Log the message only; the stack trace adds nothing for input errors.
  • Every exception inside the loop means the row is bad.