The difference being, that in one case the user has little input except for setting the goals, while deviations are ignored as much as possible, because they're considered fail-states. While in the second case, the software tries to take an interest and actually help you along as good as possible. Step by step.
But we aren't talking about any of this high logic level. When talking about exceptions I'm not talking about high level state machines, we are talking about (sub) function level code handling. Your buisnis logic states are on a much higher level and usually a form of emergent behavior of the combination of multiple functions, that reducing it to simple language construct such as exceptions is just plain meaningless.
Pascal with and without exceptions is in both cases turing complete, this means, when it comes to buisnesslogic and implementing a state machine, you can do it with or without exceptions. Your example has absolutely nothing to do about exceptions.
Just to get my point accross, I am just talking about exactly two options to handle some unexpected behavior, result or data within a function:
if not TryOperation(Parameters, OutputVar) then
begin
MyExceptionInfo:= GlobalExceptionVariable;
case MyExceptionInfo.MyExceptionInfo of
MyOperationExceptionType1: // Handle Exception of type 1
MyOperationExceptionType2: // Handle Exception of type 2
MyOperationExceptionType3: // Handle Exception of type 3
...
end
end;
// Versus:
try
OutputVar := Operation(Parameters);
except
on E: EMyException1 do // Handle Exception of type 1
on E: EMyException2 do // Handle Exception of type 2
on E: EMyException3 do // Handle Exception of type 3
...
end;
I'm just talking about the choice between return value/global variable based exception handling, as it is the typical procedural style versus the "new" exception based style. There are of course variants of the first pattern, e.g. you can do pre checks:
if not OperationCondition1 then
// Handle Exception of type 1
if not OperationCondition2 then
// Handle Exception of type 2
...
OutputVar := Operation(Parameters);
Sometimes you can encode the Exception directly in the output space:
OutputVar := Operation(Parameters);
if OutputVar = SpecialOutputCode1 then // Handle Exception of type 1
else if OutputVar = SpecialOutputCode2 // Handle Exception of type 2
...
else // use OutputVar
But you must have some way of checking if your function completed as expected, or if an exception happend. And all I'm arguing is that there are situations in which the second option (i.e. exceptions) are preferable to the first (or any of it's variants). And my argument is simple. It is very easy to forgett to do a return value check. As I have shown above in the FCL code, this can happen and stay undetected for decades. But if you are having an exception, it allows for two things, first it will "notify" you when you encounter it in the debugger, making it easier to find than if it just runs silently and destroys data. Second, because the Output of the function is undefined when it was prematurely exited. So if you forget to check the return value for a certain error, your program will just continue but with garbage in it's state.
To give an example, take this very naive StrToInt implementation, something at some point probably most programmers have written at least once:
function MyTryStrToInt(const str: String; out i: Integer): Boolean;
var
c: Char;
begin
Result := Length(str) > 0;
i := 0;
for c in str do
if c in ['0'..'9'] then
i := i * 10 + ord(c) - ord('0')
else
Exit(False);
end;
This is the extremly classical and simple approach for detecting an exception, of just returning a boolean to signal if the operation was successful. Let's say you use it to convert strings into integers, but you simply forget to check the returned boolean:
var
i: Integer;
s: String;
begin
s := '18u7'; // e.g. user just mistyped and hit u when trying to hit 7
i := 0;
MyTryStrToInt(s, i);
WriteLn(i); // prints out 18
As you can see, it prints out a value, which is the unfinished result of the algorithm, after it has been stopped half way through, so instead of the 187 which the user wanted to type in, its now 18. Any computations based on this result will not yield the results the user expects.
So to actually bring it back to your example with the navigation app. Let's say your navigation app fetches the data for the time it takes over certain routes from a webserver, and the webserver has an issue and sends an incomplete or broken result. You forget to perform validity checks (without exceptions), and instead of going 50km/h with your car, the software now thinks that you can only go 5 km/h per car, but instead that walking will be at 200 km/h.
What will the user do, who just downloaded your app to get to a certain place? Well they will just delete that app immediately and get a better one.
Alternatively assume you use exceptions. You have the same error, and you also don't handle it. So the exception bubbles up. Now when the user tries to start the search, a message shows up, which says: "Error fetching route data from the server" and when the user confirms shuts down. Of course chances are still high that the user will just uninstall and try a new one, but many users understand that network services can be unreliable at times, so some users will instead just try again, and then when the app gets a correct server answer and works as expected, they are happy.
When you ignore an exception (and you have a very basic message and crash mechanism like the LCL provides), the user will be notified about the error and why it crashes. If you ignore a return value, you will spit out completely wrong data, and be useless at best, and harmless at worst.
Do you really think that in this scenario I have outlined, the crash is worse than giving the user a completely unusable result as if it was correct?