Errores y excepciones en iOS

jueves, 5 de marzo de 2015


Las excepciones es el método común en C# y Java para gestionar los errores, sin emargo en iOS existen además del concepto de excepción (NSException) el concepto de error (NSError) y cada uno tiene un proposito y una utilidad diferente. En este artículo vamos a ver cuando recomienda Apple utilizar cada tipo de error.

Programador vs Usuario

Si quien provoca el un error es un programador o un usuario es la clave para identificar si se debe utilizar un NSError o un NSException.

Exepciones son errores de programación y este tipo de errores ocasionarían un crash en la aplicación y son anormales. Por ejemplo intentar acceder a un índice de un array que no existe, pero también provocaria una excepción crear un NSURL con algo que no es una url. En nuestras clases nosotros marcamos nuestras reglas de negocio o invariantes de forma que si tenemos una clase que recibe por parámetro un servicio al que debe llamar y es imprescidible que que no nos lo pasen a nil, deberíamos verificarlo y si nos lo han pasado a nil generamos una excepción.

Las exceptiones lo ideal es que sean detectadas y solucionadas en fase de desarrollo mediante test unitarios o en fase de pruebas y no debría ser una situación que deba pasar en producción.

Los errores sin embargo son errores originados por el usuario y son situaciones que entra dentro de lo normal que ocurran en producción. Un ejemplo claro es intentar acceder a un servicio externo en un momento donde no tenemos conexión a internet. Estos errores deben ser manejados y debemos informar al usuario de ellos. Este tipo de errores no debe ocasionar un crash de la aplicación porque son esperados.

Excepciones

Las excepciones estan representadas por la clase NSException, según esta diseñada esta clase, la idea es que sirva de forma estándar y no necesitemos realizar una clase derivada, pero es algo que vamos a poder hacer sin ningún problema. Una exception se compone de las siguientes propiedades:
  • name, string corto para identificar la excepción.
  • reason, string largo con texto amigable para identificar la exceptión.
  • userInfo, diccionario opcional y es usado para indicar información extra sobre la excepción.

Manejando exceptiones

Para manejar exceptiones en Objective-C existen las directivas de compilación @try @catch @finally .

@try {
    // código normal que puede general excepción
}
@catch (NSException* e) {
    // manejo de excepción o relanzamiento de la misma
}
@finally {
    // bloque finally
}

Excepciones predefinidas

Cocoa tiene excepciones predefinidas, pero no son clases derivadas de NSException, como hemos visto NSException esta diseñada de forma que se pueda utilizar de forma genérica para todos los casos y lo que las exceptiones predefinidas representan son el name de una clase NSException. Por ejemplo una exceptión predefinida que tiene Cocoa es NSInvalidArgumentException y es una constante almacenada en NSException.h. Aquí os dejo un enlace de apple con las excepciones predefinidas.

De esta forma cuando estamos capturando la excepción podemos utilizar los nombres de excepción predefinidos para realizar cierta lógica en función de la excepción que se ha producido.

...
} @catch(NSException *exception) {
    if (exception.name == NSInvalidArgumentException) {
        NSLog(@"Caught an NSInvalidArgumentException");
    } else {
        NSLog(@"Ignored a %@ exception", theException);
        @throw;
    }
} 

Generando excepciones

En Objetive-C también podemos generar excepciones o relanzar excepciones que se han producido. Para relanzar una excepción se utiliza la instrucción @throw.

Para generar una nueva excepción, lo mas habitual es utilizar el método de factoría exceptionWithName.
/
   NSException *e = [NSException
                     exceptionWithName:NSInvalidArgumentException
                     reason:@"value is required"
                     userInfo:nil];
   @throw e;

Errores

Los errores como ya hemos visto en la introducción son errores esperados y normales en producción, son originados por el usuario. Los errores están representadas por la clase NSError, esta clase esta diseñada para que sirva de forma estándar como las Excepciones y no necesitemos realizar una clase derivada. Un error se compone de las siguientes propiedades:
  • domain, string para identificar el error en una jerarquia. Es habitual utilizar la notación de dominio inverso junto con el nombre del proyecto tipo com.xurxo.example.DomainError.
  • code, integer númerico para identificar el error, lo normal es que este código de error sea único dentro de un dominio.
  • userInfo, diccionario opcional y es usado para indicar información extra sobre el error.
userInfo para errores es habitual que contenga más información que una excepción. Existen unas claves predefinidas para userInfo:
  • NSLocalizedDescriptionKey, string que representa la descripción del error.
  • NSLocalizedFailureReasonErrorKey, string que representa la razón del error.
  • NSLocalizedRecoverySuggestionErrorKey, string que informa al usuario de que forma puede recuperarse del error.
  • NSUnderlyingErrorKey, error origen en un subsistema, a veces puede ser interesante para extraer más información

Manejando errores

Los métodos que son susceptibles de generar un error, normalmente adeptan un parámetro por referencia indirecta de tipo NSError. Si se produce error la respuesta será NO o nil y el objeto error si se lo hemos pasado vendrá con la información del error.

El Error que pasamos por referencia indirecta es un puntero a un puntero, el método parámetro del método se define con la notación de doble puntero NSError ** y se le asigna valor con el operador de referencia.
// main.m
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString *fileToLoad = @"/path/to/non-existent-file.txt";
        
        NSError *error;
        NSString *content = [NSString stringWithContentsOfFile:fileToLoad
                                                      encoding:NSUTF8StringEncoding
                                                         error:&error];
        
        if (content == nil) {
            // Method failed
            NSLog(@"Error loading file %@!", fileToLoad);
            NSLog(@"Domain: %@", error.domain);
            NSLog(@"Error Code: %ld", error.code);
            NSLog(@"Description: %@", [error localizedDescription]);
            NSLog(@"Reason: %@", [error localizedFailureReason]);
        } else {
            // Method succeeded
            NSLog(@"Content loaded!");
            NSLog(@"%@", content);
        }
    }
    return 0;
}

Errores predefinidos

Al igual que con las excepciones existen dominios y códigos de error predefinidos en Cocoa, estan definidos en la cabecera NSError.h, podemos verlos aquí.

De esta forma cuando estamos verificando un error podemos utilizar los nombres de dominio y código predefinidos para realizar cierta lógica en función del error que se ha producido.

...
if (content == nil) {
    if ([error.domain isEqualToString:@"NSCocoaErrorDomain"] &&
        error.code == NSFileReadNoSuchFileError) {
        NSLog(@"That file doesn't exist!");
        NSLog(@"Path: %@", [[error userInfo] objectForKey:NSFilePathErrorKey]);
    } else {
        NSLog(@"Some other kind of read occurred");
    }
} ...

Generando errores

Para generar errores propios una buena práctica es tener una cabecera (.h) donde tenemos definido el dominio y un enumerado con los códigos de error.

// XurxoErrors.h

NSString *XurxoErrorDomain = @"com.xurxo.example.ErrorDomain";

enum {
    Example1Error,
    Example2Error,
};
Y entonces para generar un error utilizaremos el método de factoría errorWithDomain y necesitamos el dominio y código que ya hemos definido en la cabecera errors más un diccionario userInfo donde utlizaremos la clave predefinida NSLocalizedDescriptionKey para indicar la descripción del error.

NSString *exampleMethod(NSArray *array, NSError **error) {
    int maximum = (int)[array count];
    if (maximum == 0) {
        if (error != NULL) {
            NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Could not"
            " select a item because there are no items in the array."};
            
            *error = [NSError errorWithDomain:XurxoErrorDomain
                                         code:Example1Error
                                     userInfo:userInfo];
        }
        return nil;
    }
    int randomIndex = arc4random_uniform(maximum);
    return array[randomIndex];
}

Resumen

En este artículo hemos visto los dos conceptos de error que existen en iOS, NSError y NSException. También cual es la recomendación de Apple en el uso de cada uno de ellos, donde NSError está más enfocado a errores que es posible que puedan ocurrir en producción y NSException a errores de programación.

Libros relacionados

NSHipster: Obscure Topics in Cocoa & Objective C

iOS Programming: The Big Nerd Ranch Guide

Objective-C Programming: The Big Nerd Ranch Guide

No hay comentarios:

Publicar un comentario