With dio in a Flutter app, what do BaseOptions baseUrl, the three timeouts and validateStatus control, and how do failures surface as DioException?
answer
- defaults shared by every request
- null timeout means no limit
- 2xx passes validateStatus by default
- badResponse still carries the response
- switch on DioExceptionType
basics
~20 sBaseOptions set defaults for every request of one Dio: baseUrl, connectTimeout, sendTimeout, receiveTimeout (null means no limit) and validateStatus, which by default accepts only 2xx. Failures throw DioException with a type such as connectionTimeout, badResponse, cancel or connectionError.
solid answer
~40 s`Dio(BaseOptions(...))` holds defaults that each request merges with its own `Options`. `baseUrl` is prefixed to relative paths; `connectTimeout`, `sendTimeout` and `receiveTimeout` are `Duration?` values where `null` or `Duration.zero` means no limit, so a production app should set them. `validateStatus` decides which status codes count as success; the default accepts 200 to 299, so a 401 or 500 becomes a `DioException` of type `badResponse`, and because `receiveDataWhenStatusError` defaults to `true`, `e.response` still holds the status and body. Other types are `connectionTimeout`, `sendTimeout`, `receiveTimeout`, `badCertificate`, `cancel`, `connectionError`, `unknown` and, since 5.10, `transformTimeout`. `DioError` is only a deprecated alias of `DioException`, to be removed in 6.0.
code
dart · 29 linesimport 'package:dio/dio.dart';
final dio = Dio(
BaseOptions(
baseUrl: 'https://api.example-gym.test/v1',
connectTimeout: const Duration(seconds: 5),
sendTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 15),
),
);
Future<String> describeBooking(String id) async {
try {
final response = await dio.get<Map<String, dynamic>>('/bookings/$id');
return response.data!['className'] as String;
} on DioException catch (e) {
return switch (e.type) {
DioExceptionType.connectionTimeout ||
DioExceptionType.sendTimeout ||
DioExceptionType.receiveTimeout ||
DioExceptionType.connectionError => 'Offline or slow network',
DioExceptionType.badResponse when e.response?.statusCode == 404 =>
'No booking',
DioExceptionType.badResponse => 'Server said ${e.response?.statusCode}',
DioExceptionType.cancel => 'Cancelled',
_ => 'Unexpected error',
};
}
}go deeper
Know that BaseOptions hold baseUrl, timeouts and headers for all requests, and that failures come as DioException.
Explain the null timeout default, how validateStatus turns non-2xx into badResponse with e.response populated, and each DioExceptionType.
Set timeouts deliberately, map DioExceptionType to user-facing states in one place, and override options per request instead of globally.
Standardise error mapping across repositories so every screen reports network, auth and server failures consistently.
## Where the defaults live `dio` separates **configuration** from **calls**: - `BaseOptions` belong to one `Dio` instance and apply to every request it sends. - `Options` are passed per call (`dio.get(path, options: Options(...))`) and override the base values for that request. - Both are merged into a `RequestOptions`, the object interceptors see and that `DioException.requestOptions` carries. Create one `Dio` per backend, configure it once, and share it; each `Dio` owns its adapter and connection handling. ## The options you set on day one | Option | Default | What it controls | |---|---|---| | `baseUrl` | empty | Prefix for relative paths such as `/members/42` | | `connectTimeout` | `null` (no limit) | How long opening the connection may take | | `sendTimeout` | `null` (no limit) | How long sending the request body may take | | `receiveTimeout` | `null` (no limit) | How long receiving the response may take | | `validateStatus` | status 200 to 299 | Which statuses complete normally | | `receiveDataWhenStatusError` | `true` | Whether the body is read for failing statuses | | `responseType` | `ResponseType.json` | Whether a JSON body is decoded for you | | `headers` | empty | Headers added to every request | The source documents `null` or `Duration.zero` as "no timeout limit" for each timeout, so leaving them unset means a stalled connection can hang a screen. What values to choose is a question about the network and the backend; the dio part is knowing that nothing is enforced until you set them. Two defaults surprise people coming from `package:http`: a `Map` passed as `data` is sent as JSON (dio's built-in content-type interceptor picks `application/json`), and a JSON response is decoded, so `response.data` is already a `Map` or `List`. ## How failures arrive Every failure is thrown as a **`DioException`**. Its `type` tells you what happened: - `connectionTimeout`, `sendTimeout`, `receiveTimeout`: the matching limit elapsed. - `badResponse`: the server answered, but `validateStatus` returned `false`. `e.response` has the status code and, by default, the body. - `cancel`: a `CancelToken` cancelled the request. - `connectionError`: the connection failed, for example a `SocketException` underneath. - `badCertificate`: certificate validation rejected the server. - `transformTimeout` (added in dio 5.10): transforming the response body took too long. - `unknown`: anything else; inspect `e.error` for the underlying object. Switching on the enum turns these into user-facing states: 1. timeouts and `connectionError` become "can't reach the gym servers, try again"; 2. `badResponse` with 401 goes to the auth layer, 404 to "membership not found", 5xx to a generic error; 3. `cancel` is usually silent, because the user navigated away. ## Tuning `validateStatus` `validateStatus: (status) => status != null && status < 500` makes 4xx responses complete normally, which suits endpoints where 404 means "no booking yet" and you prefer to branch on `response.statusCode` rather than catch. Changing it on the base options affects every call, so many teams override it per request instead. ## Per-request options and typed data - `Options(responseType: ResponseType.bytes)` returns raw bytes (images, PDFs of a membership card); `ResponseType.plain` returns a `String`; `ResponseType.stream` gives a stream for large downloads. The default, `ResponseType.json`, decodes JSON bodies. - The generic parameter types `response.data`: `dio.get<Map<String, dynamic>>(...)` or `dio.get<List<dynamic>>(...)`. It is a cast of the decoded value, not a model mapper; turning maps into model classes is a separate layer. - `Options(headers: ...)` adds headers for one call, merged over the base headers. - `queryParameters` on the call are merged with any in `BaseOptions`. ## Naming history dio 5 renamed `DioError` to `DioException` and `DioErrorType` to `DioExceptionType`. The old names remain as deprecated `typedef`s marked for removal in 6.0, so new code, `catch` clauses and interceptors should use the new names.
- A 422 validation error arrives as a DioException; how do you read the server's error body?Its type is `badResponse`, and because `receiveDataWhenStatusError` defaults to `true`, `e.response` is populated: `e.response?.statusCode` is 422 and `e.response?.data` holds the decoded body. Map that to your error model.
- How do you give one slow export endpoint a longer receive timeout without changing the others?Pass per-request options: `dio.get(path, options: Options(receiveTimeout: const Duration(minutes: 2)))`. Request options override the `BaseOptions` for that call only.
- Why should new code catch DioException instead of DioError?`DioError` is now only a deprecated `typedef` for `DioException`, kept for migration and marked for removal in dio 6.0. Using the real name avoids deprecation warnings and a break at the next major version.
saying these in an interview costs you the question
- dio applies sensible timeouts by default, so none need setting.
- A 404 from dio completes normally like package:http.
- After a badResponse, the response body is lost.
- DioError and DioException are different exception types.
- Changing BaseOptions.validateStatus affects only the next request.