- Proposal: SE-TBD
- Authors: Dave DeLong, Tim Vermeulen, Erica Sadun
- Review Manager: TBD
- Status: Implemented
This proposal introduces a namespaced umbrella for common exit scenarios. In doing so, it modernizes fatalError
, a holdover from C-like languages.
Discussion: Introducing Namespacing for Common Swift Error Scenarios
Swift's fatalError
is a one-size-fits-all hammer. Swift lacks a modern extensible solution that differentiates fatal error scenarios indicating the specific contexts under which the error occurred. Because of this, many developer libraries now implement custom exit points, like the one shown in the following example, to provide that functionality:
/// Handles unimplemented functionality with site-specific information
/// courtesy of Soroush Khanlou, who discovered it from Caleb Davenport,
/// who discovered it from Brian King
func unimplemented(_ function: String = #function, _ file: String = #file) -> Never {
fatalError("\(function) in \(file) has not been implemented")
}
Namespacing these marker functions to a common type results in groupable, extensible, and discoverable IDE elements.
There are four universal core language concepts that can be better represented in Swift. They are:
- This code is unreachable.
- This code is not yet implemented.
- This method must be overriden in a subtype.
- This code should never be called.
Each item represents a fundamental language design scenario.
The Swift compiler cannot detect that the following code is exhaustive. It emits an error and recommends a default clause:
switch Int.random(in: 0...2) {
case 0: print("got 0")
case 1: print("got 1")
case 2: print("got 2")
}
// error: switch must be exhaustive
// note: do you want to add a default clause?
The default clause should annotate the code, making it clear that the clause is unreachable and should never run. If it does run, a serious breach in logic has occurred. It should result in a fatal outcome.
In Swift, TODO
items have not yet been implemented or completed. An unimplemented type member may be reachable but calling it should trap at runtime because the logic has not yet been implemented:
public func semanticallyResonantMethodName() {
// TODO: implement this
}
This call should establish why the code cannot yet be run. Swift's compile-time diagnostics are insufficient as they don't provide runtime suport:
- Using
#error
prevents compilation and debugging. - Using
#warning
is insufficient. It can be missed at compile time and error in "warn-as-errors" development houses.
Swift should provide a way to trap at run-time to indicate an currently-unsafe avenue of execution.
An abstract supertype may require methods and properties to be implemented in concrete subtypes:
class AbstractSupertype {
func someKeyBehavior() {
// Must override in subclass
}
}
This method should never be called, as the type is abstract. Again, this should trap at runtime to indicate the need for a subclass override.
Silencing warnings must not contradict Swift safety best practices. Some required members may add functionality that should never be called:
required init?(coder: NSCoder) {
super.init(coder: coder)
// Xcode requires unimplemented initializer
}
A method that is left deliberately unimplemented rather than as a TODO
item should be annotated as such and produce an appropriate runtime experience when accessed in code.
Overload the global fatalError
function as fatalError(because:)
and introduce FatalReason
, a new type that vends static members to error under these universal scenarios. The precise wording and choice of each member and error message can be bikeshedded after acceptance of this proposal's concept.
import Foundation
extension Never {
/// Reasons why code should die at runtime
public struct FatalReason: CustomStringConvertible {
/// Die because this code branch should be unreachable.
public static let unreachable = FatalReason("This code should never be reached during execution.")
/// Die because this method or function has not yet been implemented.
public static let notYetImplemented = FatalReason("This functionality is not yet implemented.")
/// Die because a default method must be overriden by a subtype
/// or extension.
public static let subtypeMustOverride = FatalReason("This type member must be overriden in subtype.")
/// Die because this functionality should never be called,
/// typically to silence requirements.
public static let mustNotBeCalled = FatalReason("This functionality should never be called.")
/// An underlying string-based cause for a fatal error.
public let reason: String
/// Establishes a new instance of a `FatalReason` with a string-based explanation.
public init(_ reason: String) {
self.reason = reason
}
/// Conforms to CustomStringConvertible, allowing reason to
/// print directly to complaint.
public var description: String {
return reason
}
}
}
/// Unconditionally prints a given message and stops execution.
///
/// - Parameters:
/// - reason: A predefined `FatalReason`.
/// - function: The name of the calling function to print with `message`. The
/// default is the calling scope where `fatalError(because:, function:, file:, line:)`
/// is called.
/// - file: The file name to print with `message`. The default is the file
/// where `fatalError(because:, function:, file:, line:)` is called.
/// - line: The line number to print along with `message`. The default is the
/// line number where `fatalError(because:, function:, file:, line:)` is called.
@inlinable // FIXME(sil-serialize-all)
@_transparent
public func fatalError(
because reason: Never.FatalReason,
function: StaticString = #function,
file: StaticString = #file,
line: UInt = #line
) -> Never {
fatalError("\(function): \(reason)", file: file, line: line)
}
You call fatalError(because:)
using the static entry points:
fatalError(because: .unreachable)
fatalError(because: .notYetImplemented)
fatalError(because: .subtypeMustOverride)
fatalError(because: .mustNotBeCalled)
A few points about this design:
- The text is intentionally developer-facing and unlocalized, as with other warning and error content.
- The contextual members (
function
,file
, andline
) are automatically picked up by the defaulted properties and passed tofatalError
. - The
FatalReason
namespacing allows each reason to be auto-completed by a hosting IDE. - The
FatalReason
namespace encourages additions specific to fatal outcomes. As its name suggests, it is not a shared repository for developer-facing strings. - The
FatalReason
namespace is parented withinNever
as a logical and thematically fitting location.
This design allows you to extend FatalReason
to introduce custom causes of programmatic run-time failure common to your in-house work, such as:
// Custom outlet complaint
extension Never.FatalReason {
static func missingOutlet(_ name: String) -> FatalReason {
return FatalReason("Outlet \"\(name)\" is not connected")
}
}
// for example:
fatalError(because: .missingOutlet("myCheckbox"))
This proposal is strictly additive. There is no reason to deprecate or remove fatalError
.
None
-
Introduce a standalone type (such as
Abort
,Crash
,Fatal
orTrap
) and a static method to avoid using a global function. This approach is less intuitive and breaks the mold established by the currentfatalError
function. -
Reconfigure the
fatalError
callsite to drop an external label, allowingfatalError("some string")
andfatalError(.someReason)
. -
Extend
Never
directly instead of introducing a new subtype.Never.because()
andNever.reason()
are confusing to read.Never.die()
is contradictory.
Thanks Ian Keen, Brent Royal-Gordon, Lance Parker, Lily Vulcano.