In Dart, what does the cascade .. do, what value does a cascade expression produce, and how does it differ from method chaining?
answer
- same receiver, every section
- section results are thrown away
- no need to return this
- can't cascade on void
- ..headers[...] = on a Request
basics
~10 sDart's cascade receiver..member runs a sequence of calls, setters or index assignments on one object, discards each result, and evaluates to the receiver itself, so any API gets fluent style without methods returning this.
solid answer
~40 sA cascade evaluates its receiver once and applies every `..` section to that same object, ignoring what each section returns; the whole expression's value is the receiver. That differs from chaining, where `a.b().c()` calls `c()` on whatever `b()` returned, so chaining only works on APIs designed to return `this`. Because sections can be setters and index assignments, you can build a `package:http` `Request` with `..headers['content-type'] = 'application/json'..body = jsonEncode(data)` in one expression and bind it to a `final`. Two traps: you cannot cascade on a `void` result such as `sb.write('a')..write('b')`, and `[3, 1, 2]..sort()` evaluates to the list, not to `sort()`'s missing result.
code
dart · 17 linesimport 'dart:convert';
import 'package:http/http.dart' as http;
Future<http.Response> postJson(
http.Client client,
Uri url,
Map<String, Object?> payload, {
required String token,
}) async {
final request = http.Request('POST', url)
..headers['content-type'] = 'application/json'
..headers['authorization'] = 'Bearer $token'
..body = jsonEncode(payload);
final streamed = await client.send(request);
return http.Response.fromStream(streamed);
}go deeper
Recall the shape: receiver..member..member, every section acting on the same object. Know that the Paint and builder-style examples are the classic uses.
Explain that the expression's value is the receiver and section results are discarded, contrast it with chaining, and name the void-receiver compile error.
Use cascades to build fully configured objects bound to final variables, such as HTTP requests or painters, and spot the precedence trap where a cascade attaches to a whole conditional.
Weigh readability in shared code: when a long cascade hides mutation of shared state, and whether to enable cascade_invocations or rely on avoid_single_cascade_in_expression_statements.
## What a cascade is A **cascade** is written `receiver..member`. Dart evaluates the receiver **once**, performs the member access, call or assignment on it, **throws away that operation's result**, and makes the value of the whole expression the **receiver itself**. Several sections can follow one another: ```dart final paint = Paint() ..color = const Color(0xFF000000) ..strokeWidth = 4 ..style = PaintingStyle.stroke; ``` This is exactly equivalent to creating `paint` and then writing three statements against it. The dart.dev documentation adds that, strictly speaking, `..` is not an operator but part of the syntax, which is why it can hold assignments and index assignments that an ordinary expression chain cannot. ## Cascade versus method chaining | | Method chaining `a.b().c()` | Cascade `a..b()..c()` | |---|---|---| | Receiver of `c()` | whatever `b()` returned | always `a` | | Needs a fluent API | yes, `b()` must return `this` or a builder | no, works on any object | | Value of the expression | the result of `c()` | `a` | | Can include setters | only as the final step | yes, any number: `..field = value`, `..map[key] = value` | Effective Dart says to **avoid returning `this` just to enable a fluent interface**, because cascades already give every API that style for free; the `avoid_returning_this` lint encodes it. Chaining is still right when each call genuinely returns a new value, as with `Iterable` operations like `where` and `map`. ## Where it earns its place: a request built in one expression A small HTTP helper that builds a `Request` from `package:http` is a natural fit. The request needs a method, a URL, several headers and a body before `Client.send` can take it: ```dart final request = http.Request('POST', url) ..headers['content-type'] = 'application/json' ..headers['authorization'] = 'Bearer $token' ..body = jsonEncode(payload); final streamed = await client.send(request); ``` Without the cascade you would write four statements and repeat `request` four times. With it, the object is fully configured at the moment it is bound to a `final` variable. Setting the header explicitly matters: the `body` setter in `package:http` adds a `text/plain` Content-Type when none has been set yet. ## The traps 1. **The value is the receiver, not the last result.** `var s = StringBuffer()..write('a')..toString();` makes `s` a `StringBuffer`; the string from `toString()` is discarded. 2. **You cannot cascade on `void`.** In `sb.write('foo')..write('bar');` the receiver is the result of `sb.write('foo')`, which is `void`, so the analyzer reports that `write` isn't defined for `void`. Put the cascade on `sb` itself: `sb..write('foo')..write('bar');`. 3. **The cascade grabs the whole expression to its left.** Cascades have lower precedence than `?:`, so `cond ? a : b..add(x)` cascades on the result of the conditional, not on `b` alone. Parenthesise the part you mean. 4. **Nesting needs parentheses.** An inner builder that should be configured and then built must be written `(Builder()..x = 1).build()`, as the dart.dev nested-cascade example shows. 5. **A single cascade in a statement is noise.** The `avoid_single_cascade_in_expression_statements` lint, in the `recommended` and `flutter` lint sets, flags `o..m();` because `o.m();` says the same thing. ## Useful idioms - **Mutating methods that return `void`**: `final sorted = [3, 1, 2]..sort();` binds the sorted list, because `sort()` returns nothing but the cascade returns the list. - **Configure-then-return**: `return <String, String>{}..addAll(defaults)..addAll(overrides);`. - **Tests and painters**: `CustomPainter.paint` implementations and test fixtures that set many fields on one object read best as a cascade. - **The `cascade_invocations` lint** (not in the default sets) suggests a cascade whenever consecutive statements call members on the same reference. ## What an interviewer listens for - That the cascade expression's value is the **receiver**, and each section's result is discarded. - The contrast with chaining, and why that makes fluent `return this` APIs unnecessary. - The `void` receiver error and the precedence trap with `?:`. - A realistic example, such as building a request or a `Paint`, rather than a toy. The null-shorting form `?..` belongs with Dart's null-aware operators and is covered there.
- Why does Effective Dart discourage methods that return this just to allow chaining?Because the cascade already gives every Dart API a fluent style: `buffer..write('one')..write('two')` works on any object, whatever the methods return. Returning `this` only for chaining adds noise to signatures and misleads readers into thinking a new object comes back. The `avoid_returning_this` lint flags it, with exceptions such as operators and methods that return a different type.
- How do you nest one cascade inside another in Dart?Wrap the inner cascade in parentheses so its receiver is clear, then continue: `(AddressBuilder()..street = s..city = c).build()` builds the inner object, and the outer cascade can assign that result to a field with `..address = (...)`. Without parentheses the sections attach to the outer receiver instead.
- In a Dart cascade, what happens to the value returned by a section such as ..toString()?It is evaluated and discarded. Each cascade section runs for its effect on the receiver; only the receiver becomes the expression's value. So `StringBuffer()..write('a')..toString()` still yields the `StringBuffer`. If you need the returned value, call it outside the cascade: `(StringBuffer()..write('a')).toString()`.
saying these in an interview costs you the question
- Thinks the cascade returns the last section's result
- Believes cascades only work on constructor calls
- Adds return this to every method to allow chaining
- Writes sb.write('a')..write('b') and expects it to compile
- Treats .. and method chaining as the same thing