Why writing to closed stream doesn't error

I have been tracking down a bug which where a stream as not open when write happened. MWE:

io = open("/tmp/test", "w")
close(io)
print(io, "test")               # why no error?

Though the docstrings do not explicitly say that this will work even when the stream is closed, I find it surprising behavior, because the behavior stated in the docstring does not happen either. Is this intended? What’s the rationale?

What is “the behavior stated in the docstring”?

I couldn’t find any mention of what happens when writing to a closed IO:

  1. The docstring of print([io::IO], xs...) says nothing about this and links to show: “print falls back to calling the 2-argument show(io, x) for each argument x in xs”.
  2. The docstring of show(io::IO, mime, x) doesn’t mention writing to a closed IO either and says that “the function body calls write (or similar) to write that representation of x to io”.
  3. The docstring of write(io::IO, x) doesn’t say anything about writing to a closed io either and links back to print.
  4. “No documentation found for public binding Core.IO.”
  5. IOStream doesn’t mention closed streams.
  6. The docstring for close(stream) doesn’t explain what it means to “Close an I/O stream” and what happens if one tries to interact with a closed stream.

basically, `?print

Write to io …

the rest are details. Usually, when a docstring says “do x”, it is understood that not doing x is an exception, ie an error.

But I don’t want to language lawyer here, I am wondering if others find it surprising, and whether it should be reported in an issue. Ie I am more interested in a practical discussion than whether arguing about the docstrings.

  • I always expected operations on closed streams to error, this is a bug
  • I never thought about it much, but now that you mention it, they should probably error, but let’s discuss
  • No, this is fine, operations on closed streams should just silently fail
  • Operations on closed streams are “undefined behavior”, they can fail or not
0 voters

After some investigation:

  1. print(io::IO, s::Union{String,SubString{String}}) simply calls write and always returns nothing.
  2. write(io::IO, s::Union{String,SubString{String}}) calls unsafe_write, which returns an integer (the number of bytes written). So print just wastes the returned value of write.

In the case of a closed IO, write fails to write and returns zero:

julia> let
        io = open("/tmp/test", "w")
        close(io)
        @which write(io, "test")
       end
write(io::IO, s::Union{SubString{String}, String})
     @ Base strings/io.jl:246

julia> let
        io = open("/tmp/test", "w")
        close(io)
        write(io, "test")
       end
0

BTW, writing zero bytes isn’t exactly an error because I guess it’s technically valid to write an empty string, thus correctly writing zero bytes.

print seems to literally be print(io::IO, s::Union{String,SubString{String}}) = (write(io, s); nothing), so it never checks what write returned.

I think write(::IO, x) should somehow indicate an error if the number of bytes written by unsafe_write does not equal the number of bytes in x.

1 Like

Speaking of the “operations on closed streams should just silently fail” option, writing to a closed IOBuffer predictably errors:

julia> let
        io = IOBuffer()
        close(io)
        write(io, "test")
       end
ERROR: ArgumentError: ensureroom failed, IOBuffer is not writeable
Stacktrace:
 [1] ensureroom_reallocate(io::IOBuffer, nshort::UInt64)
   @ Base ./iobuffer.jl:618
 [2] ensureroom
   @ ./iobuffer.jl:603 [inlined]
 [3] unsafe_write(to::IOBuffer, p::Ptr{UInt8}, nb::UInt64)
   @ Base ./iobuffer.jl:823
 [4] write(io::IOBuffer, s::String)
   @ Base ./strings/io.jl:246
 [5] top-level scope
   @ REPL[11]:4

The error message is clear too: “IOBuffer is not writeable”.

It could be interesting to investigate what happens when writing to other subtypes of IO: do they raise errors? Do they silently fail?

1 Like

This is a bug in write. It should error if it cannot write the full output, not just return zero. The error should then have details on why the write failed or if it was a partial write the error would contain the number of bytes written.

2 Likes