Why must a Go csv.Writer be flushed with Flush and then checked with Error?
answer
- the bytes are not on disk yet
- one of these methods returns nothing at all
- the file ends mid-row
- the error is sticky — you have to ask for it
basics
~20 scsv.Writer buffers records, so Write can return nil while the destination is already failing, and unflushed rows never reach the file at all. Flush pushes the buffer out but returns nothing, so Error is what reports the failure.
solid answer
~50 sA `csv.Writer` formats each record into an internal `bufio.Writer` rather than writing straight through. So `Write` returning nil means "formatted and buffered", not "on disk", and the error it does return is a sticky one from some *earlier* flush of that buffer. Two consequences. If you never call `Flush`, the last partial buffer is lost and the file ends mid-row — the classic truncated export. And `Flush` itself returns nothing, so even flushing does not tell you whether the write succeeded; `Error()` is the accessor that reports any error from a previous `Write` or `Flush`. The correct shutdown is `w.Flush()`, then `w.Error()`, then close the file and check that too. `defer w.Flush()` on its own gets the bytes out but silently swallows a full disk or a broken pipe. `WriteAll` is the shortcut: it writes everything, flushes, and returns the error itself.
code
go · 12 linesw := csv.NewWriter(f)
for _, rec := range records {
if err := w.Write(rec); err != nil { // sticky error from an earlier flush
return err
}
}
w.Flush() // returns nothing
if err := w.Error(); err != nil {
return err
}
return f.Close()go deeper
Remember the two-step ending: call Flush when you are done writing, then call Error and check it. Without the Flush the tail of the file is simply missing.
Explain the buffering: Write fills a buffer, its error is a sticky one from an earlier flush, Flush returns nothing, and Error is the only accessor that reports what went wrong.
Demonstrate the full shutdown order on a real export — Flush, Error, Close, each checked — and be able to explain why a job that logged a row count still shipped a truncated file.
Set the expectation that an export is not done until its bytes are durable and verified: what the job asserts before declaring success, and whether a truncated file can be detected downstream rather than by the recipient.
## The buffering chain `csv.NewWriter(w io.Writer)` returns a `*csv.Writer` that holds a `*bufio.Writer` wrapped around your destination. Every `Write(record []string)` call quotes the fields as needed, joins them with `Comma`, appends the line terminator, and puts the bytes in that buffer. Bytes leave the buffer only when it fills up or when you flush. That single design choice explains everything else about the API. ## Why Write's error is not the write's error ```go func (w *Writer) Write(record []string) error ``` This returns an error, and people reasonably assume it means "this record failed". It does not. Almost always the record was simply copied into the buffer and nothing touched the file. When it *does* return non-nil, it is reporting a failure that happened during an earlier automatic flush — the buffered writer holds its first error and keeps returning it. So a loop that dutifully checks every `Write` error still cannot conclude the export succeeded, because the final buffer's worth of records has not been written yet when the loop ends. (There is one immediate failure: an invalid `Comma` — a newline, a carriage return, or an invalid rune — makes `Write` return an error before writing anything.) ## Why Flush cannot report it either ```go func (w *Writer) Flush() ``` No return value. This is deliberate — it makes `defer w.Flush()` legal and tidy — but it means the moment when the bytes actually meet the disk is the moment you have no error to inspect. Whatever went wrong is recorded inside the writer. ```go func (w *Writer) Error() error ``` `Error` reports any error that occurred during a previous `Write` or `Flush`. It is the only way to learn the truth, and it must be called *after* the final `Flush`. ## The correct shutdown sequence ```go w := csv.NewWriter(f) for _, rec := range records { if err := w.Write(rec); err != nil { return err } } w.Flush() if err := w.Error(); err != nil { return err } return f.Close() ``` Three checks, in order: the loop catches an early sticky failure and stops wasting work; `Flush` + `Error` catches the failure of the final buffer; closing the file catches whatever the operating system deferred until then. Reporting success without all three is reporting a guess. ## What the bug looks like in production An export job runs, logs "wrote 48,213 rows", exits 0. The partner reports that the file ends halfway through a row, or is missing the last few hundred records. Nothing in your logs is red, because the only code path that could have gone red — the final flush — either never ran or ran without being asked whether it worked. Disk-full is the same story with the error thrown away instead of the bytes. The near-miss version is worse: `defer w.Flush()` *does* run, the bytes *do* get out most of the time, and the one time the disk is full you still report success. ## WriteAll ```go func (w *Writer) WriteAll(records [][]string) error ``` `WriteAll` writes every record, calls `Flush` itself, and returns the error — so a checked `WriteAll` needs no separate `Error` call and is the right choice when you already hold all the records. What it costs you is that you must hold them: a `[][]string` of the entire output in memory, which is exactly the footprint you were avoiding by streaming rows out of a query or an import. ## The mental model to keep "Buffered writer" is a general idea, but the Go specifics are what get asked: `Write`'s error is sticky and retrospective, `Flush` returns nothing at all, `Error` is the reporter, and `WriteAll` folds the flush in. Once those four facts are in place, the shutdown sequence writes itself, and so does the code review comment on the next `defer w.Flush()` you see standing alone.
- Is `defer w.Flush()` enough at the end of an export function?No. It gets the buffered bytes out, but Flush returns nothing, so a failure at that exact moment — a full disk, a broken pipe, a closed socket — disappears. Flush explicitly, call Error and check it, then close the file and check that too. Only then can the function report success honestly.
- Does WriteAll change any of this?Yes. WriteAll writes every record, flushes, and returns the resulting error, so a checked WriteAll needs no separate Error call. The tradeoff is that it wants the whole output as a [][]string in memory, which defeats the point when you are streaming rows out of a database or an import loop.
- Does closing the underlying os.File flush the csv.Writer?No. Close is a method on the file, and it knows nothing about the buffer sitting in the csv.Writer above it. Closing first simply discards whatever is still buffered. The order is always Flush, then check Error, then Close the file and check its error too.
saying these in an interview costs you the question
- Assumes Write puts the record straight on disk
- Uses defer w.Flush() and never calls Error
- Thinks Flush returns an error
- Reports success because every Write returned nil
- Believes closing the file flushes the csv.Writer