Why disposing Utf8JsonWriter leaves file locked and how to fix it

Summary

The Utf8JsonWriter you create with new(File.Create(...)) owns the underlying `FileStream, and disposing the writer does not close that stream. The file remains locked, so the subsequent File.OpenRead fails with “the process cannot access the file … because it is being used by another process.” The fix is to also dispose the FileStream (or use a using block) or let JsonSerializer.Serialize manage the stream for you.

Root Cause

  • File.Create(path) returns a FileStream opened with FileShare.None.
  • Passing that stream to Utf8JsonWriter gives the writer ownership of the writer only, not the stream.
  • Calling relevantDataWriter.Dispose() flushes buffers but does not dispose the underlying FileStream.
  • The FileStream stays open and locked, causing the read attempt to throw.

Why This Happens in Real Systems

  • Implicit resource ownership: many APIs (including Utf8JsonWriter) expect the caller to manage the lifetime of the stream they are given.
  • FileShare defaults: File.Create uses FileShare.None, which prevents any other handle from opening the file until the original handle is closed.
  • Assumption that Dispose cascades: developers often assume disposing a wrapper object will also close the wrapped stream, which is not true for Utf8JsonWriter.

Real-World Impact

  • Test suites hang or fail intermittently when they try to read files that were just written.
  • Production services that generate JSON logs or payloads may lock files, leading to file‑access errors for monitoring or archival tools.
  • Resource leakage: open file handles accumulate, eventually hitting the OS limit and causing process crashes.

Example or Code (if necessary and relevant)

// WRONG: only the writer is disposed
Utf8JsonWriter writer = new(File.Create(filePath));
JsonSerializer.Serialize(writer, data);
writer.Flush();
writer.Dispose();   // <-- leaves FileStream open

// CORRECT: dispose the FileStream as well, preferably with using
using FileStream fs = File.Create(filePath);
using Utf8JsonWriter writer = new(fs);
JsonSerializer.Serialize(writer, data);

How Senior Engineers Fix It

  • Wrap both the stream and the writer in using statements so they are disposed together.
  • Prefer the overload JsonSerializer.Serialize<T>(Stream, T, JsonSerializerOptions?) which handles stream disposal automatically.
  • If you must create the writer yourself, pass a stream opened with appropriate sharing flags, e.g. new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read).
  • Add unit‑test cleanup that asserts the file is not locked after the write operation.

Why Juniors Miss It

  • They often confuse the lifetime of the wrapper object with the underlying stream.
  • They rely on the IDE’s quick‑fix “dispose” that generates Dispose() calls for the immediate object only.
  • Limited exposure to file‑sharing semantics (FileShare.None vs. FileShare.Read) leads to the assumption that a disposed writer releases the file.
  • Lack of habit to use using blocks or the newer using var syntax for deterministic resource cleanup.

Leave a Comment