# Should noncopyable types release system resources in \`deinit\`?

**URL:** <https://forums.swift.org/t/should-noncopyable-types-release-system-resources-in-deinit/90120>\
**Category:** Discussion\
**Tags:** swift-system\
**Created:** [October 16, 2026, 6:06pm UTC](https://forums.swift.org/t/should-noncopyable-types-release-system-resources-in-deinit/90120 "2026-10-16T18:06:53Z")\
**Posts on this page:** 1\
**Showing post:** 49

<div class="post-metadata">

**Author:** ![jrflat](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jrflat/32/31407_2.png) [@jrflat](https://forums.swift.org/u/jrflat)\
**Post date:** [October 29, 2026, 4:30pm UTC](https://forums.swift.org/t/should-noncopyable-types-release-system-resources-in-deinit/90120/49 "2026-10-29T16:30:15Z")

</div>

I'd like to summarize the arguments here to help us come closer to a decision.

Linear types (`~Deinitable`) would make a forgotten `close()` a compile error, and many of us prefer compile-time enforcement ([#12](https://forums.swift.org/t/90120/12), [#17](https://forums.swift.org/t/90120/17), [#21](https://forums.swift.org/t/90120/21), [#25](https://forums.swift.org/t/90120/25), [#39](https://forums.swift.org/t/90120/39)). However, linear types aren't guaranteed to happen, or to happen any time soon ([#14](https://forums.swift.org/t/90120/14), [#15](https://forums.swift.org/t/90120/15)). Agreeing that System's new types are best expressed as linear types could help prioritize the feature ([#14](https://forums.swift.org/t/90120/14)).

### Narrowing the question

Let's assume that `~Deinitable` isn't available yet. A noncopyable resource type's `deinit` can then trap or close. (A `deinit` that does neither silently leaks resources, so we'll ignore that case.) Either option can still trap on an ownership violation, such as a close that fails with `EBADF` ([pitch #31](https://forums.swift.org/t/90024/31)).

How do we decide whether **Trap** or **Close** is better?

Let's assume our type has a consuming, throwing `close()` that reports any error from the underlying close function (e.g. `close` or `CloseHandle`). Using this `close()`, the two options differ only when a handle reaches its `deinit`, where **Trap** crashes and **Close** closes the handle. Any code that works under **Trap** , including its error handling, would behave and perform identically under **Close**.

So the better option depends on what developers _will_ write, not what they _can_ write. Most benefits of **Trap** come from the fact that a developer or library author must write an explicit `close()` on every path, or else the app/server will crash.

Each argument below is followed by a counterargument. Expand the summaries to see more details and sources.

### Arguments for Trap

* * *

> **Trap: Explicit close() calls leave a syntactic trace that makes lifetimes easier to reason about.**
>
> - Swift requires `try` and `await` so that readers can see the effects. A close can block, fail, and change kernel state, so a close deserves the same visibility ([pitch #22](https://forums.swift.org/t/90024/22)).
> - Handles are limited, so programs need to control exactly when they're released ([#19](https://forums.swift.org/t/90120/19), [pitch #14](https://forums.swift.org/t/90024/14)).
> - An innocuous-looking change can move an implicit close. The change's diff might not show where the handle was opened ([#17](https://forums.swift.org/t/90120/17), [pitch #21](https://forums.swift.org/t/90024/21)).
> 
> For example, suppose only one branch closes the handle. On the other path, `deinit` closes the handle where the branches rejoin ([#38](https://forums.swift.org/t/90120/38), [#43](https://forums.swift.org/t/90120/43)):
> 
> ```swift
> let handle = try Win32.FileHandle.open(...)
> if shouldClose {
> try? handle.close()
> }
> // If `shouldClose` is false, `deinit` closes `handle` here, before `finish()`
> finish()
> 
> ```

> **Close: Code that needs a precise close point can still call close(), and the compiler then keeps that point from moving.**
>
> - An explicit `close()` at the end of a scope makes any earlier consume a compile error ([pitch #16](https://forums.swift.org/t/90024/16)).
> - A handle that's never consumed closes where a `defer { try? handle.close() }` written right after its `open` would run, if `defer` could consume the handle ([pitch #16](https://forums.swift.org/t/90024/16)).
> - Today, a handle consumed on only one path closes where the paths rejoin. John says that's intended ([#42](https://forums.swift.org/t/90120/42)), and Alejandro calls it a bug ([#41](https://forums.swift.org/t/90120/41)). Kavon offered to move that close to the end of the scope in a new move checker ([#46](https://forums.swift.org/t/90120/46)).
> - Unlike a missing `try`, a missing `close()` isn't a compile error with either option. **Trap** only catches a missing `close()` on paths that run.

* * *

> **Trap: Requiring an explicit close() encourages developers to perform error handling.**
>
> - The developer must write `try handle.close()` or acknowledge the discarded error with `try? handle.close()`.
> - Swift doesn't let you omit `try` just because you plan to ignore the error ([#22](https://forums.swift.org/t/90120/22)).
> - A closing `deinit` always loses the error ([pitch #21](https://forums.swift.org/t/90024/21)).
> - On some platforms, close can report a deferred write error ([#6](https://forums.swift.org/t/90120/6)). A program could log that error or show it to the user ([#34](https://forums.swift.org/t/90120/34)).

> **Close: A program can rarely act on a close error, and developers who wish to perform error handling will do so anyway.**
>
> - Linux, the BSDs, and Darwin release the descriptor even when close fails, so a failed close shouldn't be retried ([#13](https://forums.swift.org/t/90120/13), [#37](https://forums.swift.org/t/90120/37)).
> - Many closes can't fail in any meaningful or actionable way; **Trap** adds complexity and noise for these cases ([#23](https://forums.swift.org/t/90120/23), [#31](https://forums.swift.org/t/90120/31)).
> - To catch lost writes, a program has to call `fsync` or `FlushFileBuffers` before closing. Neither option enforces those calls ([#13](https://forums.swift.org/t/90120/13)).
> - A developer who can't act on the error will write `try? handle.close()` at the end of the scope. That `try?` is equivalent to a closing `deinit`, except that the `deinit` also avoids accidental crashes on error paths ([pitch #7](https://forums.swift.org/t/90024/7)).

* * *

> **Trap: The ecosystem should follow one pattern for non-memory resources, and some resources require trapping in deinit.**
>
> - The pattern should also cover clients, requests, and queries ([#17](https://forums.swift.org/t/90120/17)).
> - Many resources have `async` or throwing cleanup that `deinit` can't express ([#17](https://forums.swift.org/t/90120/17), [#38](https://forums.swift.org/t/90120/38)).
> - Some resources must be released from a specific isolation. Isolated `deinit`s likely won't come to noncopyable types ([#12](https://forums.swift.org/t/90120/12)).
> - RAII should only be used for resources whose release can't fail except by programmer error ([pitch #24](https://forums.swift.org/t/90024/24)). The release also shouldn't block for long and should either be opt-in or have no observable timing.

> **Close: One rule fits every resource: trap only where deinit can't release the resource correctly.**
>
> - Under that rule, `Continuation` and NIO's `async` file handle keep their traps. Windows handles and Mach ports close in `deinit` ([#7](https://forums.swift.org/t/90120/7)).
> - The choice should depend on the resource. Many resources can be released without reporting real effects to the caller ([#20](https://forums.swift.org/t/90120/20)).
> - SE-0390's motivating example is a noncopyable `FileDescriptor` whose `deinit` closes the descriptor ([#36](https://forums.swift.org/t/90120/36)). Franz and others repeatedly objected to such resource examples in the `~Copyable` proposals ([#38](https://forums.swift.org/t/90120/38)).

* * *

> **Trap: Automatic disposal should be an opt-in wrapper, not the default.**
>
> - The default should be that the code holding a handle is responsible for closing it. Delegating that responsibility, for example with a wrapper like `RAII<FileDescriptor>`, should be an explicit choice ([pitch #21](https://forums.swift.org/t/90024/21)).

> **Close: A wrapper can't opt a developer out of crashes in someone else's code.**
>
> - An app's wrapper doesn't cover handles that a library opens or accepts. Whether those handles trap or close is the library author's choice, not the app developer's. If **Trap** is the default, a missed `close()` in a library can crash the app, and only the library's author can fix the crash.
> - With **Close** as the default, the opt-in goes the other way. SYS-0011 suggests a trapping wrapper for those who want it, and only code that adopts the wrapper is affected.

* * *

> **Trap: Trapping keeps our options open, since relaxing a trap later is not source-breaking.**
>
> - A closing `deinit` can't start trapping later without crashing clients that rely on the implicit close.
> - Code written for **Trap** already closes on every path like a future `~Deinitable` type would require.

> **Close: Adopting ~Deinitable later would be source-breaking with either option.**
>
> - A `~Deinitable` type would no longer fit APIs that take `~Copyable` types, unless they're generalized ([#14](https://forums.swift.org/t/90120/14)). Those APIs include `Optional`, which **Trap**'s workarounds use.
> - Aviva suggested staging in `~Deinitable` while keeping the existing `deinit` for code that hasn't migrated ([#15](https://forums.swift.org/t/90120/15)). That approach works whether the `deinit` traps or closes.
> - Even once `~Deinitable` exists, a read-only file stream may not need it, since its close can't fail in any meaningful way ([#23](https://forums.swift.org/t/90120/23)).

### Arguments for Close

* * *

> **Close: Crashing the app/server on a forgotten close() is a poor user experience that can be avoided.**
>
> - A crash may lose other work or user data that's not related to the crashing handle.
> - Certain user configurations may lead to constant crashes and inability to use the app or service.
> - These crashes would mainly happen in untested error paths, so many would first happen in production ([#7](https://forums.swift.org/t/90120/7), [#39](https://forums.swift.org/t/90120/39)).

> **Trap: Most forgotten closes are caught during development.**
>
> - A forgotten `close()` on a common path crashes as soon as that code path runs ([#17](https://forums.swift.org/t/90120/17)).
> - Crash reports help the developer or library author fix the rest.

* * *

> **Close: Closing in deinit fits Swift's goal of progressive disclosure: simple uses stay simple, and developers can add explicit closes and error handling as they need them.**
>
> - The [swift.org](https://www.swift.org) home page says Swift's "progressive disclosure allows you to pick up the language quickly, taking advantage of power-user features as you need them."
> - With **Close** , a simple use can open a handle and let `deinit` close it, even on error paths.
> - Code that needs the close error can call `close()`. Code that needs durability can call `fsync` or `FlushFileBuffers` first. Code that wants every close to be explicit can adopt a trapping wrapper.
> - With **Trap** , even the simplest use must close the handle explicitly on every path, including error paths. John notes that explicitly destroying a value on every path is "really laborious," even with compiler checking ([#20](https://forums.swift.org/t/90120/20)).

> **Trap: Progressive disclosure shouldn't hide effects. Swift requires try and await even in simple code.**
>
> - The compiler knows which calls throw or suspend, but Swift still requires the markers so that readers can follow the control flow ([pitch #22](https://forums.swift.org/t/90024/22)).

* * *

> **Close: With Trap, seemingly innocuous changes can crash the process if errors aren't handled within scope. Handling those errors locally takes cumbersome do/catch blocks that close the handle and rethrow.**
>
> In a function that already throws, adding a `try` between an open and a close looks harmless ([#24](https://forums.swift.org/t/90120/24)):
> 
> ```diff
> let handle = try Win32.FileHandle.open(...)
> +try someMethod()
> try handle.close()
> 
> ```
> 
> But if `someMethod()` throws, the process now traps instead of propagating the error. The diff doesn't show the new crash.
> 
> To propagate the error, **Trap** requires a local `do`/`catch` that closes the handle and rethrows. (Wrapping the handle in an `Optional` so `defer` can `take()` it is another workaround.)
> 
> ```swift
> let handle = try Win32.FileHandle.open(...)
> do {
> try someMethod()
> } catch {
> try? handle.close() // Ignores the error anyway
> throw error
> }
> try handle.close()
> 
> ```

> **Trap: Scoped helpers and future language features could close the handle on error paths.**
>
> - Franz agrees the added-`try` problem should be solved, but not with implicit closing ([#30](https://forums.swift.org/t/90120/30)).
> - Scoped helpers would eliminate the `do`/`catch` in most cases ([#2](https://forums.swift.org/t/90120/2), [#21](https://forums.swift.org/t/90120/21)).
> - David suggests the clutter is "tertiary to correctness and clarity" ([#21](https://forums.swift.org/t/90120/21)).
> - The pitched at-most-once functions feature should make a consuming `defer` possible ([#3](https://forums.swift.org/t/90120/3)).
> - A `with` syntax, like Python's context managers, could cover remaining use cases ([#30](https://forums.swift.org/t/90120/30), [#38](https://forums.swift.org/t/90120/38)).

> **Close: Scoped helpers and with blocks can't cover a handle that a function returns, and they must turn errors from the body and the close into one thrown error.**
>
> - A function can't return a handle that a helper or `with` block manages ([#35](https://forums.swift.org/t/90120/35)).
> - A consuming `defer` can't be combined with `return handle` or an explicit `try handle.close()`. The handle would be consumed twice, which doesn't compile ([pitch #16](https://forums.swift.org/t/90024/16)).
> - Only a `defer` that skips an already-consumed handle would work. A closing `deinit` already does that ([pitch #7](https://forums.swift.org/t/90024/7)).
> - Swift doesn't let a `defer` throw, so a consuming `defer` couldn't propagate the close error ([#23](https://forums.swift.org/t/90120/23)).
> - A helper's closure must also carry every effect, including the thrown error type ([#2](https://forums.swift.org/t/90120/2), [#17](https://forums.swift.org/t/90120/17)). David suggests untyped throws for the error type ([#4](https://forums.swift.org/t/90120/4)).
> - When the body and the close both throw, a helper or `with` block can only throw one error. System's `FileDescriptor.closeAfter(_:)` and Franz's `with` sketch both drop the close error ([#30](https://forums.swift.org/t/90120/30)), just as a closing `deinit` does.

> **Trap: In the future, ~Deinitable could cover the handles that helpers can't.**
>
> - Franz suggests eventually adopting both context managers and `~Deinitable` ([#38](https://forums.swift.org/t/90120/38)). Context managers would handle most code. `~Deinitable` would give compile-time checks to code that wants manual control.

* * *

> **Close: Language limitations prevent some owners from closing a handle explicitly.**
>
> - A class or `InlineArray` can't consume a stored handle ([pitch #1](https://forums.swift.org/t/90024/1)).
> - A handle captured by a task can't be consumed afterward ([pitch #1](https://forums.swift.org/t/90024/1)).
> - A `let` handle that lends a nonescapable view can't be consumed afterward either ([#16](https://forums.swift.org/t/90120/16)).
> - With **Trap** , these owners need a workaround, like an `Optional` or a `Mutex` box. Otherwise, the process crashes.

> **Trap: Swift should lift those limitations instead of designing APIs around them.**
>
> - Franz would recommend the trapping pattern more broadly once Swift has call-once closures and task groups that support `~Copyable` values ([#17](https://forums.swift.org/t/90120/17)).
> - Franz argued the same about `defer`: its limitation is an argument for a consuming `defer`, not for `deinit` ([pitch #5](https://forums.swift.org/t/90024/5)).

* * *

I've tried to include the main arguments from this discussion and the pitch thread, plus a few that hadn't come up yet. Please feel free to pick them apart or add your own!

---

_[View the full topic](https://forums.swift.org/t/should-noncopyable-types-release-system-resources-in-deinit/90120)._
