skip to content

Explain File.copyTo, File.copyRecursively, and File.deleteRecursively in kotlin.io: their overwrite semantics, return values, error handling, and the key caveats (atomicity, the missing-file behavior) you must warn a team about.

level: seniorimportance: should knowfreq 35%

answer

  1. copyTo: overwrite=false throws FileAlreadyExistsException
  2. copyTo creates missing parents, returns target
  3. copyRecursively: top-down, onError -> SKIP/TERMINATE
  4. deleteRecursively: bottom-up, returns Boolean, no throw
  5. None atomic; deleteRecursively returns true if path absent

basics

~20 s

copyTo copies one file to a target; copyRecursively copies a whole folder; deleteRecursively deletes a folder and everything in it. By default copyTo refuses to overwrite an existing target and throws; pass overwrite = true to replace it. deleteRecursively returns true/false instead of throwing.

solid answer

~40 s

File.copyTo(target, overwrite = false, bufferSize): copies a single file's bytes to target, creating missing parent directories; if target exists and overwrite is false it throws FileAlreadyExistsException; with overwrite=true it replaces it. It returns the target File. File.copyRecursively(target, overwrite, onError): walks the source top-down copying every entry; onError returns OnErrorAction (SKIP / TERMINATE) so you can decide per-failure; default action rethrows. It returns Boolean (false if terminated). File.deleteRecursively(): deletes the file/tree using a bottom-up walk and returns Boolean — true if everything was deleted, false otherwise; it does NOT throw on failure and returns true for a non-existent path. Critical caveats: none of these are atomic (a crash mid-copy leaves partial state), deleteRecursively isn't safe against concurrent modification or symlink trickery, and the boolean return is easy to ignore.

code

kotlin · 14 lines
kotlin
import java.io.File

val ok = File("src").copyRecursively(
    target = File("dst"),
    overwrite = false,
    onError = { file, e ->
        if (e is FileAlreadyExistsException) OnErrorAction.SKIP
        else OnErrorAction.TERMINATE
    },
)
if (!ok) println("copy terminated early")

// deleteRecursively never throws -> always check the Boolean
check(File("dst").deleteRecursively()) { "cleanup failed" }

go deeper

for a junior

Knows copyTo copies a file and deleteRecursively removes a folder and its contents.

for a middle

Knows the overwrite flag and the FileAlreadyExistsException, and that deleteRecursively returns a Boolean.

for a senior

Explains onError/OnErrorAction, the non-throwing Boolean of deleteRecursively (true on missing path), and that copyTo creates parents.

for a principal

Calls out non-atomicity, TOCTOU/symlink risks, ignored-Boolean failure modes, and prescribes java.nio (ATOMIC_MOVE, temp-then-rename) for production-grade file operations.

## copyTo — single file ```kotlin fun File.copyTo(target: File, overwrite: Boolean = false, bufferSize: Int = DEFAULT_BUFFER_SIZE): File ``` - Copies the **source file's bytes** to `target`, **creating missing parent directories** of the target. - If `target` **already exists** and `overwrite == false`, it throws **`FileAlreadyExistsException`**. With `overwrite == true` it overwrites (deleting an existing directory target first if needed). - If the **source does not exist**, it throws **`NoSuchFileException`**. - **Returns** the `target` `File`. - Copies **content only** — not all file attributes/permissions are guaranteed preserved (use `java.nio.file.Files.copy` with options for attribute/atomic needs). ## copyRecursively — whole tree ```kotlin fun File.copyRecursively( target: File, overwrite: Boolean = false, onError: (File, IOException) -> OnErrorAction = { _, e -> throw e }, ): Boolean ``` - Performs a **top-down walk** of the source, recreating each directory and copying each file under `target`. - `onError` is invoked per failure and returns an **`OnErrorAction`**: **`SKIP`** (continue) or **`TERMINATE`** (stop). The default lambda **rethrows**. - **Returns `Boolean`**: `false` if the walk was terminated by `TERMINATE`, otherwise `true`. ## deleteRecursively — delete a tree ```kotlin fun File.deleteRecursively(): Boolean ``` - Deletes the file or directory tree using a **bottom-up walk** (so directories are emptied before removal). - **Returns `Boolean`** — `true` if **all** entries were deleted, `false` if **any** deletion failed. - It **does NOT throw** on failure — you must **check the return value**. - On a **non-existent** path it returns **`true`** (nothing left to delete), which surprises people expecting an error. ## The caveats to warn a team about 1. **Not atomic.** A crash or exception mid-operation leaves a **partial** copy or partial delete. For atomic replace use `java.nio.file.Files.move(..., ATOMIC_MOVE)` / a temp-then-rename pattern. 2. **Boolean returns are silently droppable.** `deleteRecursively()` and `copyRecursively()` return a result that linters won't always force you to read; ignoring it hides failures. 3. **Concurrency / TOCTOU.** Files added during a recursive delete may be missed; symlinks can cause a delete/copy to escape the intended tree on untrusted input. 4. **Permissions/attributes** are not fully preserved by `copyTo`. 5. **deleteRecursively follows the walk**, so a symlinked directory could lead it outside the tree — validate on untrusted paths. ## When to reach for java.nio instead For atomic moves, attribute preservation, `REPLACE_EXISTING`/`COPY_ATTRIBUTES` options, or symlink-aware deletion, prefer `java.nio.file.Files` (`copy`, `move`, `walkFileTree`). The `kotlin.io` helpers are convenient for tests/tooling but make weaker guarantees. ```kotlin val src = File("/in/report.csv") val dst = File("/out/2026/report.csv") src.copyTo(dst, overwrite = true) // creates /out/2026, replaces dst val ok = File("/tmp/work").deleteRecursively() if (!ok) error("failed to clean /tmp/work") // never ignore the Boolean ```

  • What exception does copyTo throw when the target exists and overwrite is false?
    FileAlreadyExistsException. Pass overwrite = true to replace the target instead.
  • Does deleteRecursively throw if the path doesn't exist?
    No. It returns true (nothing to delete) and never throws; you check the Boolean for partial failures.
  • How would you make a copy effectively atomic?
    Copy to a temp file in the same directory, then Files.move with ATOMIC_MOVE/REPLACE_EXISTING to swap it into place.

copyTo is like saving a file and refusing to clobber an existing one unless you tick 'replace'; deleteRecursively is a quiet shredder that reports success/failure rather than raising an alarm.

saying these in an interview costs you the question

  • Thinking copyTo overwrites by default
  • Assuming deleteRecursively throws on failure instead of returning false
  • Ignoring the Boolean return of deleteRecursively/copyRecursively
  • Claiming these operations are atomic
  • Not knowing copyRecursively's onError returns SKIP/TERMINATE

context