Errors and recovery

rustfits raises Python’s standard built-in exceptions — ValueError, IndexError, KeyError, IOError, NotImplementedError — rather than defining its own FITS* exception hierarchy. This page covers which error type you’ll see in which situation, and the file-integrity model that underlies the I/O errors.

Why standard exceptions?

A custom FITSError hierarchy would let users catch “anything from this library” with one except clause. In practice that’s worth less than it sounds: the operations that fail in rustfits are the same operations that fail anywhere — opening a missing file (OSError), passing a wrong dtype (ValueError), running past the end of an array (IndexError). Using the built-ins means:

  • Existing error-handling code keeps working (except OSError picks up file-system failures from rustfits the same way it does from open / numpy / anything else).

  • No new exception class to learn or import.

  • isinstance(e, ValueError) is the only filter you need.

The trade-off is that you can’t filter “all rustfits errors” in a single except. That’s been a non-issue in practice; if it ever matters we’d add FITSError as a marker base class.

What raises what

Exception

Typical causes

ValueError

Unknown column name; dtype mismatch on write; shape mismatch; unsupported key shape on __setitem__; writing to a protected header keyword (BITPIX, NAXIS, TFORMn, …); malformed input; calling repack() on a file with non-default THEAP. The catch-all “you gave me something I can’t use.”

IndexError

Out-of-range integer index — row index past the end of a table, pixel index past the end of an image, HDU index past len(fits). Slice indexing is bounds-safe (clips to the array) per numpy convention; only bare-int access raises.

KeyError

Missing header keyword on header[key] lookup. Matches dict semantics. Use key in header or header.get(key, default) to avoid.

IOError

File-system failures (OSError subclass): file doesn’t exist on mode="r" / "r+"; file is locked by another process; write failed mid-flush (e.g. ENOSPC); the per-file taint flag is set (see below).

NotImplementedError

You called a method on a stub HDU type (read), or asked for a feature listed in Known limitations as “(not yet)”.

The taint flag and recovery

FITS is a sequential format with no transactional layer. Most write operations in rustfits follow a prepare in RAM, write to disk last discipline so that an error before the write leaves the file untouched. But once the write has started and a write_all or flush call fails partway through — typically ENOSPC or EIO — the file may be in a partially-written state where the in-memory cards/data no longer match what’s on disk.

When that happens, rustfits sets a per-file taint flag and every subsequent operation on any view of that file refuses with IOError naming the inconsistency. This includes reads through the same handle, reads through any HDU or FITSHeader reference you kept around, and any future writes.

The recovery path is intentionally simple: close the file and reopen it. The fresh handle re-parses the on-disk state from scratch, and the taint flag (which is per-handle, not stored on disk) is gone.

import rustfits

try:
    with rustfits.FITS("data.fits", "r+") as fits:
        fits[1].header["object"] = "M31"
        fits[1].write(big_array)        # may fail on ENOSPC
except IOError as e:
    print(f"write failed: {e}")
    # The file is now in whatever state the OS left it.
    # Reopen to see what actually landed.
    with rustfits.FITS("data.fits", "r") as fits:
        # ... inspect, decide whether to retry or recover.

Operations that taint:

  • The chunked file-tail shift during header / data grow (shift_file_tail_and_update_offsets).

  • rewrite_header_to_disk’s write_all / flush.

  • write_image_data / write_table_data / their compressed counterparts after any byte has hit the disk.

Operations that DON’T taint (the file is still consistent afterward):

  • Opening a missing or locked file.

  • The slack-overflow precheck inside header rewrite — if there isn’t enough room for the new cards and the file can’t grow, it raises BEFORE touching disk.

  • Any ValueError / IndexError / KeyError from input validation — those run before any I/O.

  • Calling a stub method (NotImplementedError) — no I/O happens.

So the rule of thumb is: ValueError / IndexError etc. mean “the file is fine, fix the call and retry”; IOError means “stop, close, reopen, inspect.”

The taint flag is per-handle, not per-file-on-disk. Two different FITS handles opening the same file have independent taint state.

Within one Python process, rustfits serializes file access through an internal mutex on each open handle, so two threads calling hdu.write(...) on the same FITS object don’t race. rustfits does not take an OS-level file lock, though — two separate processes (or two separate rustfits.FITS() calls in the same program) writing to the same file concurrently will corrupt it. Multi-writer workflows need to coordinate at a higher level (e.g. fcntl.flock).