Files, JSON and HTTP in daily Dart
Read and write files with dart:io, decode JSON safely, make HTTP requests with timeouts, and retry the failures worth retrying.
Files and directories
import 'dart:convert';
import 'dart:io';
Future<void> main() async {
// read a whole file as text; encoding defaults to UTF-8
final file = File('data/config.json');
if (await file.exists()) {
final text = await file.readAsString();
print(text.length);
}
// write, creating the parent directory first
final out = File('out/report.txt');
await out.parent.create(recursive: true);
await out.writeAsString('header\n', mode: FileMode.write);
// append to an existing file
await out.writeAsString('row 1\n', mode: FileMode.append);
// read line by line without loading the whole file
await for (final line in file.openRead().transform(utf8.decoder).transform(const LineSplitter())) {
if (line.contains('ERROR')) print(line);
}
// walk a directory tree
await for (final entity in Directory('data').list(recursive: true)) {
if (entity is File && entity.path.endsWith('.csv')) {
print(await entity.length());
}
}
// path manipulation with the platform separator handled for you
final p = '${Directory.current.path}${Platform.pathSeparator}data';
print(p);
// delete and rename
if (await out.exists()) await out.delete();
print(Platform.isWindows);
}- All of
dart:iois asynchronous and returnsFutures. There are no synchronous variants worth using exceptexistsSyncin a script's setup code. - Reading a very large file with
readAsStringholds it entirely in memory. UseopenReadwith a transformer for streaming. FileMode.appendadds to the end;FileMode.writetruncates. Forgetting this loses the file's contents.FileSystemExceptioncarries the OS error code and the path, which is what you want in a log.Platform.pathSeparatorandDirectory.currentkeep a script working on Windows and on Linux.
JSON decoding and encoding
import 'dart:convert';
class Order {
Order(this.id, this.sku, this.quantity);
final String id;
final String sku;
final int quantity;
// a factory from JSON: validate as you parse, and fail with a useful message
factory Order.fromJson(Map<String, dynamic> json) {
final id = json['id'];
final sku = json['sku'];
final qty = json['quantity'];
if (id is! String || sku is! String || qty is! int) {
throw FormatException('unexpected order shape', jsonEncode(json));
}
return Order(id, sku, qty);
}
Map<String, dynamic> toJson() => {'id': id, 'sku': sku, 'quantity': quantity};
@override
String toString() => 'Order($id, $sku, $quantity)';
}
void main() {
const raw = '''
{"id":"o1","sku":"ABC-1","quantity":2,
"lines":[{"sku":"ABC-1","qty":2}]}
''';
final decoded = jsonDecode(raw) as Map<String, dynamic>;
final order = Order.fromJson(decoded);
print(order);
// nested access requires casts; jsonDecode gives dynamic, not a typed tree
final lines = (decoded['lines'] as List).cast<Map<String, dynamic>>();
print(lines.map((l) => l['sku']).toList());
// encoding: pass toEncodable for types the encoder does not know
print(jsonEncode(order));
print(jsonEncode({'items': [order]}, toEncodable: (o) => o is Order ? o.toJson() : o.toString()));
// a stream of JSON objects: one per line, decoded as they arrive
final objects = const LineSplitter()
.convert('{"a":1}\n{"a":2}\n')
.map((l) => jsonDecode(l) as Map<String, dynamic>);
print(objects.map((m) => m['a']).toList());
// decoding failure is a FormatException, not a null
try {
jsonDecode('{not json}');
} on FormatException catch (e) {
print('bad json at offset ${e.offset}: ${e.message}');
}
}💡
Write the
fromJson factory by hand for anything that crosses a trust boundary, and check each field's type with is. Generated serialisation code is convenient, but a hand-written parser gives you the validation and the error message that a production service needs.HTTP requests
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
Future<List<String>> fetchSkus(http.Client client) async {
final uri = Uri.https('api.example.com', '/v1/products', {'limit': '50', 'q': 'abc'});
try {
final response = await client
.get(uri, headers: {'Accept': 'application/json'})
.timeout(const Duration(seconds: 10));
if (response.statusCode != 200) {
throw HttpException('unexpected ${response.statusCode}: ${response.body}');
}
final body = jsonDecode(response.body) as Map<String, dynamic>;
final items = (body['items'] as List).cast<Map<String, dynamic>>();
return items.map((i) => i['sku'] as String).toList();
} on TimeoutException {
throw HttpException('request timed out');
} on SocketException catch (e) {
throw HttpException('network unreachable: ${e.message}');
}
}
Future<void> post(http.Client client) async {
final response = await client.post(
Uri.https('api.example.com', '/v1/orders'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'sku': 'ABC-1', 'quantity': 2}),
);
print(response.statusCode);
}
Future<void> main() async {
// reuse one client: a new connection per request is slow and leaks sockets
final client = http.Client();
try {
print(await fetchSkus(client));
} finally {
client.close();
}
}
class TimeoutException implements Exception {
final String message = 'timeout';
}
class HttpException implements Exception {
HttpException(this.message);
final String message;
@override
String toString() => 'HttpException: $message';
}| Failure | Typical cause | Retry? |
|---|---|---|
SocketException | No network, DNS failure, host down | Yes, with backoff |
TimeoutException | Slow server or network | Yes, once or twice |
HandshakeException | TLS certificate problem | No: fix the certificate |
| Status 400 or 422 | The request body is wrong | No: fix the request |
| Status 401 or 403 | Missing or bad credentials | No, until the token is refreshed |
| Status 429 | Rate limited | Yes, after the delay the server asks for |
| Status 5xx | Server fault | Yes, with backoff and a cap |
Retry only idempotent requests by default. A POST that may have been processed before the connection dropped needs an idempotency key, or a retry creates a duplicate order.
FAQ
Should I use http or dio?
The
http package is enough for straightforward requests and has fewer moving parts. A richer client such as dio adds interceptors, request cancellation, form data and progress callbacks, which pays off in an application with authentication and uploads.Why does Dart complain about reading a file on the web?
dart:io is not available on the web platform. Keep file and socket code behind an interface with a conditional import so the same package compiles for the browser and for the VM.Related
Errors, exceptions and async error handling Command-line tools: args, stdin and exit codes
Last refreshed 2026-09-18.