Catch an Exception for Invalid User Input in Swift

Catch an exception for invalid user input in swift

This is still an issue in Swift 2. As noted, the best solution is to use a bridging header and catch the NSException in Objective C.

https://medium.com/swift-programming/adding-try-catch-to-swift-71ab27bcb5b8 describes a good solution, but the exact code doesn't compile in Swift 2 because try and catch are now reserved keywords. You'll need to change the method signature to workaround this. Here's an example:

// https://medium.com/swift-programming/adding-try-catch-to-swift-71ab27bcb5b8

@interface TryCatch : NSObject

+ (void)tryBlock:(void (^)())try catchBlock:(void (^)(NSException *))catch finallyBlock:(void (^)())finally;

@end

@implementation TryCatch

+ (void)tryBlock:(void (^)())try catchBlock:(void (^)(NSException *))catch finallyBlock:(void (^)())finally {
@try {
try ? try() : nil;
}
@catch (NSException *e) {
catch ? catch(e) : nil;
}
@finally {
finally ? finally() : nil;
}
}

@end

Is invalid user input a valid reason for throwing an exception?

Generally speaking, invalid or ill-formed input is not consider 'exceptional' and should be handled using something other than exceptions. But note that this is a guideline - there may well be situations where using exceptions to handle the problem would result in better code.

try-catch exceptions in Swift

It doesn't have exception handling, and this discussion in the developer forum discusses why it may be so:

but keep in mind that Cocoa and Cocoa Touch traditionally don't intend
for you to catch exceptions; they intend for you to not cause them to
be thrown in the first place. Ordinary errors should be handled with
optional types and inout NSError parameters; you should address any
situation that causes an assertion to fail (which seems to be the only
exception-throwing mechanism in Swift) by writing better code.

Catching NSException in Swift

Here is some code, that converts NSExceptions to Swift 2 errors.

Now you can use

do {
try ObjC.catchException {

/* calls that might throw an NSException */
}
}
catch {
print("An error ocurred: \(error)")
}

ObjC.h:

#import <Foundation/Foundation.h>

@interface ObjC : NSObject

+ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error;

@end

ObjC.m

#import "ObjC.h"

@implementation ObjC

+ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error {
@try {
tryBlock();
return YES;
}
@catch (NSException *exception) {
*error = [[NSError alloc] initWithDomain:exception.name code:0 userInfo:exception.userInfo];
return NO;
}
}

@end

Don't forget to add this to your "*-Bridging-Header.h":

#import "ObjC.h"

NSExpression catch invalid arguments

I think NSExpression is not the right tool for the job here. The class is part of the Cocoa predicate system and was designed to only accept well-formatted input.

I suggest you look for a proper math parser. I believe GCMathParser is a good choice. There's also DDMathParser.

If you insist on using NSExpression, you can catch the exception like this:

@try {
// the code that potentially raises an NSInvalidArgumentException
} @catch (NSException *exception) {
if ([[exception name] isEqualToString:NSInvalidArgumentException]) {
// your error handling
}
}

Be advised, however, that this is bad practice. Exceptions in Objective-C should only be used to catch unexpected runtime errors. Your example does not qualify.

What is the exception that should be thrown when user input is blank?

Tcl doesn't have a pre-defined hierarchy of exceptions. The throw command takes 2 arguments: type is a list of words; and message is an error message for humans.

You could do something like

proc getJobinfo {question} {
...
if {$cleanedanswer eq ""} {
throw {Value Empty} "Please provide a suitable answer."
} elseif {[string length $cleanedanswer] < 5} {
throw {Value Invalid} "Your answer is too short."
} else ...
return $cleanedanswer
}

If you want to trap that error:

try {
set answer [getJobinfo "What is the answer to this question"]
} trap {Value *} msg {
puts "Value Error: $msg"
}

throw and try interact via the type words of the throw call. We throw "Value Empty" or "Value Invalid". In the trap, we match Value exactly, but we won't match * exactly. In hindsight the * should not be there.
The try manpage is not super clear at first read:

trap pattern variableList script

This clause matches if the evaluation of body resulted in an error and the prefix of the -errorcode from the interpreter's status dictionary is equal to the pattern. The number of prefix words taken from the -errorcode is equal to the list-length of pattern, and inter-word spaces are normalized in both the -errorcode and pattern before comparison.

pattern is not a pattern in the regexp or string match sense: it's a list of words that is matched one-by-one with the list of words thrown in the try body.

The try can be implemented with multiple traps to have cascading "catches":

try {
set answer [getJobinfo "What is the answer to this question"]

} trap {Value Empty} msg {
do something specific here

} trap {Value Invalid} msg {
do something specific here

} trap {Value} msg {
do something general for some other "throw {Value anything} msg"

} on error e {
this can be default catch-all for any other error

} finally {
any cleanup code goes here
}

Simplest way to throw an error/exception with a custom message in Swift?

The simplest approach is probably to define one custom enum with just one case that has a String attached to it:

enum MyError: ErrorType {
case runtimeError(String)
}

Or, as of Swift 4:

enum MyError: Error {
case runtimeError(String)
}

Example usage would be something like:

func someFunction() throws {
throw MyError.runtimeError("some message")
}
do {
try someFunction()
} catch MyError.runtimeError(let errorMessage) {
print(errorMessage)
}

If you wish to use existing Error types, the most general one would be an NSError, and you could make a factory method to create and throw one with a custom message.

Why the swift throws exception despite try?

try? does not catch exceptions. It catches thrown errors. Those are different things in Swift. Exceptions are at the Objective-C level and cannot be caught by Swift at all (they can't be safely caught in ObjC in most cases either, but that's a different discussion).

The solution in this case is to use JSONEncoder rather than JSONSerialization. JSONEncoder is a pure-Swift system. JSONSerialization is bridged from ObjC.

let body = try? JSONEncoder().encode([data])

See Handling Errors for more information:

Error handling in Swift resembles exception handling in other languages, with the use of the try, catch and throw keywords. Unlike exception handling in many languages—including Objective-C—error handling in Swift does not involve unwinding the call stack, a process that can be computationally expensive. As such, the performance characteristics of a throw statement are comparable to those of a return statement.

If you want to use JSONSerialization, it's important to recognize that it is a programming error to call it this way. The exception is intended to crash the program (even in ObjC). The correct way to write this code is:

if JSONSerialization.isValidJSONObject([data]), // <=== first, check it is valid
let body = try? JSONSerialization.data(withJSONObject: [data]) {
print("success")
} else {
print("unable to make body for call")
}

See the docs for more information:

If obj will not produce valid JSON, an exception is thrown. This exception is thrown prior to parsing and represents a programming error, not an internal error. You should check whether the input will produce valid JSON before calling this method by using isValidJSONObject(_:).

The thrown error from JSONSerialization is only to indicate an internal error in the serializer, not an attempt to encode an invalid object:

error

If an internal error occurs, upon return contains an NSError object with code NSPropertyListWriteInvalidError that describes the problem.



Related Topics



Leave a reply



Submit