Perhaps syntactically, but they're actually completely different at the implementation level. The implementation details then have some consequences for how exactly the feature works.
In Java, any arbitrary method can potentially throw an exception. Suppose you had:
int callee() {
// throw new RuntimeException();
return 123;
}
void caller() {
try {
var someResource = openSomeResource();
var result = callee();
System.out.println(result);
} finally {
someResource.close();
}
}
In the happy path, nothing needs to done by caller. It calls callee, gets the return result, and it goes on to print, close the resource, and exit.
But on the off-chance that the callee code throws an exception, the program needs to know how to exit out of the caller stackframe, while preserving the semantics of the program. The tricky part is that finally clause, and making sure it's executed, even if callee() unexpectedly throws an exception.
To do this, the compiler needs to produce some code which explains how to gracefully exit out of caller() from any place where an exception might be thrown (i.e., every single method call). This code is set aside, and and is never hit in the happy path. In the case where an exception is thrown, it's run. This process is called stack unwinding.
- Pro: the happy path is fast
- Pro: any callee code can raise an exception, and all will be fine
- Con: the unhappy path is much slower
- Con: the stack unwinding code causes code bloat, whether needed or not
In comparison, Swift's error-throwing mechanism is much like a return, with some implicit branching done by the caller. Because throws effects the calling convention (i.e. the caller needs to do something different when calling a throwing function), it's explicitly stated on a function using the keyword.
This is why you can only throw an error out of a function marked throws (if it wasn't marked throws and you were allowed to throw anyway, the caller wouldn't know to check for an error, and nothing would work).
- Pro: the happy path is pretty fast (only one branch, which high predictability)
- Pro: the unhappy path is equally fast (only one branch)
- Pro: no stack unwinding code
- Con: only
throw-marked functions can throw, so you can't universally throw everywhere.
- As a result, you also can't throw "through" non-throwing code. If your parent is non-throwing, then you must handle the error with
try?, try! or catch. You can't just re-throw it, because the parent can't handle it.