Skip to content

Create and clean up a temp file snippet

Create a uniquely named scratch file, use it, then delete it — the backbone of atomic-save patterns, handing work to external programs, and keeping tests from littering the filesystem.

Create a uniquely named scratch file, use it, then delete it — the backbone of atomic-save patterns, handing work to external programs, and keeping tests from littering the filesystem. The two classic failure modes are homemade names (a PID or timestamp collides the moment two processes overlap; the mkstemp-family APIs create the file exclusively so a race is impossible, your suffix is not) and cleanup that never runs (delete-on-exit hooks and 'the OS sweeps /tmp eventually' leak badly in long-lived servers and containers with a mounted tmpfs). Every example pairs creation with teardown that survives the error path. SQL is omitted: query languages expose no filesystem scratch-file API — temp storage is the engine's own business (temp tablespaces, TEMPORARY tables) and file handoff belongs to the client.

Runnable recipe · 14 languages
Files & Datatemp-filescleanupfilestmpdir

Every language

14 implementations, copy-ready. One at a time with syntax highlighting, or all inline.

JSJavaScript
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

// A private temp DIRECTORY, not a bare file: an exclusive collision-proof
// name plus room for whatever else the task needs next to it.
const dir = await mkdtemp(join(tmpdir(), 'app-'));
const file = join(dir, 'work.txt');

try {
  await writeFile(file, 'scratch data\n', 'utf8');
  console.log(file);
  // ...hand `file` to a child process, a test, a parser...
} finally {
  // force: no throw if creation half-failed; recursive: takes the dir too
  await rm(dir, { recursive: true, force: true });
}

mkdtemp appends six random characters to the prefix and creates the directory with mode 0700 — never build temp names from Date.now() or process.pid. rm with force:true makes cleanup idempotent; without it a failed writeFile lets the finally block throw ENOENT and mask the real error. Sync twin: fs.mkdtempSync.

TSTypeScript
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

/** Run `fn` with a fresh temp file path; always remove it afterwards. */
async function withTempFile(
  fn: (path: string) => Promise<void>,
): Promise<void> {
  const dir = await mkdtemp(join(tmpdir(), 'app-'));
  const path = join(dir, 'work.txt');
  try {
    await writeFile(path, 'scratch data\n', 'utf8');
    await fn(path);
  } finally {
    await rm(dir, { recursive: true, force: true });
  }
}

await withTempFile(async (path) => console.log(path));

The finally block is the whole point — it runs when fn rejects too, so the temp directory never survives a task that died mid-flight. TS 5.2+ on Node 20 offers `await using` (Symbol.asyncDispose) as the structural alternative to the callback wrapper.

GoGo
package main

import (
	"fmt"
	"os"
)

func main() {
	f, err := os.CreateTemp("", "app-*.txt") // "" = os.TempDir(), exclusive create
	if err != nil {
		panic(err)
	}
	path := f.Name()

	// LIFO: Close runs first, Remove second — required on Windows,
	// where an open file cannot be unlinked.
	defer os.Remove(path)
	defer f.Close()

	if _, err := f.WriteString("scratch data\n"); err != nil {
		panic(err)
	}
	fmt.Println(path)
}

os.CreateTemp replaced ioutil.TempFile; the * in the pattern marks where the random suffix lands (no * means append at the end). The ignored Remove error is deliberate best-effort — but treat the OS sweeping /tmp as a backstop, not the mechanism. os.MkdirTemp is the directory twin.

RsRust
use std::io::Write;

fn main() -> std::io::Result<()> {
    let mut file = tempfile::NamedTempFile::new()?;
    writeln!(file, "scratch data")?;
    println!("temp file: {}", file.path().display());

    // ...pass file.path() around — it is a real, visible path...

    Ok(())
} // `file` drops here: closed AND unlinked, even on panic-unwind

tempfile = "3" in Cargo.toml. Deletion is tied to Drop, so a ? early-return or a panicking thread still cleans up — exactly the property the manual languages rebuild with defer/finally. Call file.persist("final.txt") when the file must survive.

PHPPHP
<?php
// tmpfile(): exclusive name in the system temp dir + deletion
// registered for fclose() and script shutdown.
$fh = tmpfile();
if ($fh === false) {
    throw new RuntimeException('tmpfile() failed');
}

// It returns a stream, not a path — recover the path like this:
$path = stream_get_meta_data($fh)['uri'];

fwrite($fh, "scratch data\n");
fflush($fh);

// ...rewind($fh) to read back, or pass $path to an external command...

fclose($fh); // removes the file

The handle owns the file: fclose() (or script termination) deletes it, so there is no unlink to forget — and no way to keep the file except copying it. Need a path you control? tempnam(sys_get_temp_dir(), 'app-') creates an exclusive empty file you must unlink yourself.

PyPython
import tempfile

with tempfile.NamedTemporaryFile(
    mode="w+", encoding="utf-8", delete=True
) as f:
    f.write("scratch data\n")
    f.flush()            # contents now visible at f.name to other processes
    print(f.name)

# leaving the with-block closes AND unlinks the file

delete=True is the default: close() unlinks. Pass delete=False when the file must outlive the handle — cleanup then becomes your job. On Windows the name cannot be reopened by other code while the handle is open (Linux: it can). tempfile.TemporaryFile is the anonymous variant with no portable name.

CC
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void) {
    char path[] = "/tmp/app-XXXXXX";   /* template rewritten in place */

    int fd = mkstemp(path);            /* atomically: random name + O_EXCL open */
    if (fd == -1) {
        perror("mkstemp");
        return 1;
    }

    dprintf(fd, "scratch data\n");

    if (close(fd) == -1) {             /* flush before anyone reads it */
        perror("close");
    }
    unlink(path);                      /* must run on EVERY exit path */
    return 0;
}

mkstemp is the only race-free POSIX spelling — never tmpnam(), which hands back a name another process can create first. Real programs centralize close+unlink (or fdopen + fclose) in one helper so no error branch can skip the unlink. Windows CRT: _mktemp_s + _open(..., _O_CREAT | _O_EXCL).

C++C++
#include <filesystem>
#include <iostream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <unistd.h>

namespace fs = std::filesystem;

// RAII over mkstemp: destructor closes + unlinks on every exit path,
// including exceptions. C++ has no std temp-file facility — POSIX creates.
class TempFile {
public:
    explicit TempFile(const fs::path& pattern)
        : path_(pattern.string()) {
        fd_ = ::mkstemp(path_.data());        // rewrites the trailing XXXXXX
        if (fd_ == -1) throw std::runtime_error("mkstemp failed");
    }
    ~TempFile() {
        if (fd_ != -1) ::close(fd_);
        std::error_code ec;
        fs::remove(path_, ec);                // best-effort, never throws
    }
    TempFile(const TempFile&) = delete;
    TempFile& operator=(const TempFile&) = delete;

    void write(std::string_view bytes) {
        if (::write(fd_, bytes.data(), bytes.size()) == -1)
            throw std::runtime_error("write failed");
    }
    const std::string& path() const { return path_; }

private:
    std::string path_;
    int fd_ = -1;
};

int main() {
    TempFile t(fs::temp_directory_path() / "app-XXXXXX");
    t.write("scratch data\n");
    std::cout << t.path() << '\n';
    // ...pass t.path() to other code...
}   // closed + unlinked here

C++17+ (non-const std::string::data(), <filesystem>). POSIX only — MSVC needs _mktemp_s/_open or a UUID-named file. The deleted copy constructor is load-bearing: a copied handle would close and unlink twice.

C#C#
using System;
using System.IO;

// GetTempFileName(): creates a ZERO-BYTE file and returns its path —
// the platform guarantees the name is fresh.
string path = Path.GetTempFileName();
try
{
    File.WriteAllText(path, "scratch data");
    Console.WriteLine(path);
    // ...hand the path to another process or a test...
}
finally
{
    File.Delete(path); // throws if the file is already gone — wrap if that can happen
}

GetTempFileName cycles .tmp names and throws IOException once ~65k files sit in %TEMP% — long-running services prefer Path.Combine(Path.GetTempPath(), $"app-{Guid.NewGuid():N}.tmp") opened with FileShare.None. File.Delete on a missing path throws FileNotFoundException; catch it when failure paths may have removed the file already.

JvJava
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class CreateTempFile {
    public static void main(String[] args) throws IOException {
        Path tmp = Files.createTempFile("app-", ".txt"); // exclusive, in java.io.tmpdir
        try {
            Files.writeString(tmp, "scratch data\n");
            System.out.println(tmp);
            // ...pass tmp to other code...
        } finally {
            Files.deleteIfExists(tmp); // no throw if someone already removed it
        }
    }
}

Files.createTempFile(prefix, suffix, dir) — omit the dir and it honors java.io.tmpdir. File.deleteOnExit() is the leaky alternative: it only fires at JVM exit, so app servers that run for months accumulate files. The attributes overload sets POSIX perms when the content is sensitive.

SwSwift
import Foundation

let fm = FileManager.default
// No mkstemp equivalent in Foundation: assemble a unique name yourself.
let url = fm.temporaryDirectory
    .appendingPathComponent("app-\(UUID().uuidString).txt")

do {
    try "scratch data".write(to: url, atomically: true, encoding: .utf8)
    print(url.path)
    // ...work with the file...
} catch {
    fatalError("temp write failed: \(error)")
}

try? fm.removeItem(at: url) // idempotent; misses only a hard exit

UUID names make collisions vanish, but creation is still not exclusive — for hostile temp directories open the file with O_EXCL via Darwin/Glibc's open(2) instead. atomically: true writes a sibling file then renames: exactly the durable pattern. In real code, factor this into a function so defer { try? fm.removeItem(at: url) } also covers thrown errors.

KtKotlin
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.writeText

// Scoped resource: creation, your block, teardown — in that order.
// finally guarantees the unlink even when the block throws.
fun <R> withTempFile(
    prefix: String = "app-",
    suffix: String = ".txt",
    block: (Path) -> R,
): R {
    val tmp = Files.createTempFile(prefix, suffix)
    try {
        return block(tmp)
    } finally {
        Files.deleteIfExists(tmp)
    }
}

fun main() {
    withTempFile { tmp ->
        tmp.writeText("scratch data\n")
        println(tmp)
    }
}

Same JVM semantics as the Java impl: createTempFile is exclusive and honors java.io.tmpdir. kotlin.io.path supplies the writeText/deleteIfExists extension sugar (kotlin-stdlib jdk7 variant, the default on the JVM).

RbRuby
require 'tempfile'

# Block form: closed AND unlinked when the block ends — however it exits.
Tempfile.create('app-') do |f|
  f.write("scratch data\n")
  f.flush              # contents visible at f.path from here on
  puts f.path          # a real path — child processes can read it
end

# Tempfile.new is the manual spelling: close! unlinks at once, plain close
# leaves the file for a finalizer to remove later — too passive for servers.

Tempfile.create yields a Tempfile whose path is a real file in Dir.tmpdir; on Windows it stays locked until the block ends. Inside the block is the only safe window to use f.path — after it returns the file is gone.

ZigZig
const std = @import("std");

pub fn main() !void {
    // No mkstemp in std: build a unique name from OS randomness.
    const name = try std.fmt.allocPrint(
        std.heap.page_allocator,
        "/tmp/app-{d}.txt",
        .{std.crypto.random.int(u64)},
    );
    defer std.heap.page_allocator.free(name);

    {
        // .exclusive fails with error.PathAlreadyExists instead of
        // clobbering — with 2^64 names, retrying is academic.
        const f = try std.fs.createFileAbsolute(name, .{
            .exclusive = true,
            .truncate = false,
        });
        defer f.close();
        try f.writeAll("scratch data\n");
        std.debug.print("temp file: {s}\n", .{name});
    }

    std.fs.deleteFileAbsolute(name) catch |err| {
        std.debug.print("cleanup failed: {s}\n", .{@errorName(err)});
    };
}

std.crypto.random pulls from the OS CSPRNG. /tmp is a POSIX assumption — read std.posix.getenv("TMPDIR") in production. Cleanup here is manual and does NOT run when a try above fails mid-way; a real helper takes a block (like the Kotlin impl) so defer ordering covers every path. Zig 0.12+ API spelling.