Consideraciones de manejo de errores

31

El problema:

Desde hace mucho tiempo, estoy preocupado por el exceptions mecanismo, porque siento que realmente no resuelve lo que debería.

RECLAMACIÓN: Hay largos debates afuera sobre este tema, y ​​la mayoría de ellos tienen dificultades para comparar exceptions vs devolver un código de error. Este definitivamente no es el tema aquí.

Intentando definir un error, estaría de acuerdo con CppCoreGuidelines, de Bjarne Stroustrup & Herb Sutter

Un error significa que la función no puede lograr su propósito anunciado

RECLAMACIÓN: El exceptionmecanismo es un lenguaje semántico para manejar errores.

RECLAMACIÓN: Para mí, no hay "excusa" para que una función no logre una tarea: o definimos erróneamente las condiciones previas / posteriores para que la función no pueda garantizar resultados, o algún caso excepcional específico no se considera lo suficientemente importante como para pasar tiempo en el desarrollo una solución. Teniendo en cuenta que, en mi opinión, la diferencia entre el código normal y el manejo del código de error es (antes de la implementación) una línea muy subjetiva.

RECLAMACIÓN: El uso de excepciones para indicar cuándo no se mantiene una condición previa o posterior es otro propósito del exceptionmecanismo, principalmente para fines de depuración. No apunto este uso de exceptionsaquí.

En muchos libros, tutoriales y otras fuentes, tienden a mostrar el manejo de errores como una ciencia bastante objetiva, que se resuelve exceptionsy solo se necesita catchpara tener un software robusto, capaz de recuperarse de cualquier situación. Pero mis varios años como desarrollador me hacen ver el problema desde un enfoque diferente:

  • Los programadores tienden a simplificar su tarea lanzando excepciones cuando el caso específico parece demasiado raro para implementarlo con cuidado. Los casos típicos de esto son: problemas de falta de memoria, problemas de disco lleno, problemas de archivos corruptos, etc. Esto puede ser suficiente, pero no siempre se decide desde un nivel arquitectónico.
  • Los programadores tienden a no leer cuidadosamente la documentación sobre las excepciones en las bibliotecas, y generalmente no saben qué y cuándo se lanza una función. Además, incluso cuando lo saben, realmente no los manejan.
  • Los programadores tienden a no detectar excepciones lo suficientemente temprano, y cuando lo hacen, es principalmente para iniciar sesión y lanzar más. (consulte el primer punto).

Esto tiene dos consecuencias:

  1. Los errores que ocurren con frecuencia se detectan temprano en el desarrollo y se depuran (lo cual es bueno).
  2. Las excepciones raras no se gestionan y hacen que el sistema se bloquee (con un buen mensaje de registro) en la página de inicio del usuario. Algunas veces se informa el error, o ni siquiera.

Teniendo en cuenta que, el objetivo principal de un mecanismo de error de la OMI debería ser:

  1. Hacer visible en el código donde no se gestiona un caso específico.
  2. Comunique el tiempo de ejecución del problema al código relacionado (al menos la persona que llama) cuando ocurra esta situación.
  3. Proporciona mecanismos de recuperación.

El principal defecto de la exceptionsemántica como mecanismo de manejo de errores es la OMI: es fácil ver dónde throwestá a en el código fuente, pero no es absolutamente evidente saber si una función específica podría arrojarse al mirar la declaración. Esto trae todo el problema que presenté anteriormente.

El lenguaje no aplica y verifica el código de error tan estrictamente como lo hace para otros aspectos del lenguaje (por ejemplo, tipos fuertes de variables)

Un intento de solución

Con la intención de mejorar esto, desarrollé un sistema de manejo de errores muy simple, que intenta poner el manejo de errores en el mismo nivel de importancia que el código normal.

La idea es:

  • Cada función (relevante) recibe una referencia a un successobjeto muy ligero, y puede establecerlo en un estado de error en el caso. El objeto es muy ligero hasta que se guarda un error con el texto.
  • Se alienta a una función a omitir su tarea si el objeto proporcionado ya contiene un error.
  • Un error nunca debe anularse.

El diseño completo obviamente considera a fondo cada aspecto (aproximadamente 10 páginas), también cómo aplicarlo a OOP.

Ejemplo de la Successclase:

class Success
{
public:
    enum SuccessStatus
    {
        ok = 0,             // All is fine
        error = 1,          // Any error has been reached
        uninitialized = 2,  // Initialization is required
        finished = 3,       // This object already performed its task and is not useful anymore
        unimplemented = 4,  // This feature is not implemented already
    };

    Success(){}
    Success( const Success& v);
    virtual ~Success() = default;
    virtual Success& operator= (const Success& v);

    // Comparators
    virtual bool operator==( const Success& s)const { return (this->status==s.status && this->stateStr==s.stateStr);}
    virtual bool operator!=( const Success& s)const { return (this->status!=s.status || this->stateStr==s.stateStr);}

    // Retrieve if the status is not "ok"
    virtual bool operator!() const { return status!=ok;}

    // Retrieve if the status is "ok"
    operator bool() const { return status==ok;}

    // Set a new status
    virtual Success& set( SuccessStatus status, std::string msg="");
    virtual void reset();

    virtual std::string toString() const{ return stateStr;}
    virtual SuccessStatus getStatus() const { return status; }
    virtual operator SuccessStatus() const { return status; }

private:
    std::string stateStr;
    SuccessStatus status = Success::ok;
};

Uso:

double mySqrt( Success& s, double v)
{
    double result = 0.0;
    if (!s) ; // do nothing
    else if (v<0.0) s.set(Error, "Square root require non-negative input.");
    else result = std::sqrt(v);
    return result;
}

Success s;
mySqrt(s, 144.0);
otherStuff(s);
saveStuff(s);
if (s) /*All is good*/;
else cout << s << endl;

Lo utilicé en muchos de mis (propios) códigos y obligó al programador (a mí) a pensar más sobre posibles casos excepcionales y cómo resolverlos (bien). Sin embargo, tiene una curva de aprendizaje y no se integra bien con el código que ahora lo usa.

La pregunta

Me gustaría entender mejor las implicaciones de usar tal paradigma en un proyecto:

  • ¿Es correcta la premisa del problema? o ¿Me perdí algo relevante?
  • ¿Es la solución una buena idea arquitectónica? o el precio es demasiado alto?

EDITAR:

Comparación entre métodos:

//Exceptions:

    // Incorrect
    File f = open("text.txt"); // Could throw but nothing tell it! Will crash
    save(f);

    // Correct
    File f;
    try
    {
        f = open("text.txt");
        save(f);
    }
    catch( ... )
    {
        // do something 
    }

//Error code (mixed):

    // Incorrect
    File f = open("text.txt"); //Nothing tell you it may fail! Will crash
    save(f);

    // Correct
    File f = open("text.txt");
    if (f) save(f);

//Error code (pure);

    // Incorrect
    File f;
    open(f, "text.txt"); //Easy to forget the return value! will crash
    save(f);

    //Correct
    File f;
    Error er = open(f, "text.txt");
    if (!er) save(f);

//Success mechanism:

    Success s;
    File f;
    open(s, "text.txt");
    save(s, f); //s cannot be avoided, will never crash.
    if (s) ... //optional. If you created s, you probably don't forget it.
Adrian Maire
fuente
25
Votado como "Esta pregunta muestra el esfuerzo de investigación; es útil y claro", no porque esté de acuerdo: creo que algunos de los pensamientos están equivocados. (Los detalles pueden seguir en una respuesta.)
Martin Ba
2
¡Absolutamente, entiendo y estoy de acuerdo con eso! El propósito de esta pregunta es ser criticado. Y el puntaje de la pregunta para indicar preguntas buenas / malas, no es que el OP sea correcto.
Adrian Maire
2
Si entiendo correctamente, su queja principal sobre las excepciones es que las personas pueden ignorarlo (en c ++) en lugar de manejarlas. Sin embargo, su construcción Success tiene el mismo defecto por diseño. Como excepciones, simplemente lo ignorarán. Peor aún: es más detallado, conduce a retornos en cascada y ni siquiera puedes "atraparlo" río arriba.
Dagnelies
3
¿Por qué no usar algo como mónadas? Hacen implícitos sus errores, pero no permanecerán en silencio durante la ejecución. En realidad, lo primero que pensé al mirar su código fue "mónadas, agradable". Míralos.
bash0r
2
La razón principal por la que me gustan las excepciones es que le permiten detectar todos los errores inesperados de un bloque de código determinado y manejarlos de manera consistente. Sí, no hay una buena razón por la cual el código no debe realizar su tarea: "hubo un error" es una mala razón, pero aún sucede , y cuando sucede, desea registrar la causa y mostrar un mensaje, o volver a intentarlo. (Tengo un código que hace una interacción compleja y reiniciable con un sistema remoto; si el sistema remoto se cae, quiero registrarlo y volver a intentarlo desde el principio)
user253751

Respuestas:

32

El manejo de errores es quizás la parte más difícil de un programa.

En general, darse cuenta de que hay una condición de error es fácil; sin embargo, señalarlo de una manera que no se pueda eludir y manejarlo adecuadamente (ver los niveles de seguridad de excepción de Abrahams ) es realmente difícil.

En C, los errores de señalización se realizan mediante un código de retorno, que es isomorfo a su solución.

C ++ introdujo excepciones debido a la deficiencia de tal enfoque; a saber, solo funciona si las personas que llaman recuerdan verificar si se produjo un error o no, y de lo contrario, falla de manera horrible. Cada vez que te encuentras diciendo "Está bien siempre y cuando ..." tienes un problema; los humanos no son tan meticulosos, incluso cuando les importa.

El problema, sin embargo, es que las excepciones tienen sus propios problemas. A saber, flujo de control invisible / oculto. Esto estaba destinado: ocultar el caso de error para que la lógica del código no se ofusque por la placa de control de errores. Hace que el "camino feliz" sea mucho más claro (¡y rápido!), A costa de hacer que los caminos de error sean casi inescrutables.


Me resulta interesante observar cómo otros idiomas abordan el problema:

  • Java ha marcado excepciones (y no marcadas),
  • Go usa códigos de error / pánico,
  • Rust usa tipos de suma / pánico).
  • Lenguajes FP en general.

C ++ solía tener alguna forma de excepciones comprobadas, es posible que haya notado que se ha desaprobado y simplificado hacia un básico en su noexcept(<bool>)lugar: o se declara que una función posiblemente se lanza, o se declara que nunca se debe. Las excepciones marcadas son algo problemáticas ya que carecen de extensibilidad, lo que puede causar asignaciones / anidaciones incómodas. Y jerarquías de excepciones enrevesadas (uno de los principales casos de uso de herencia virtual son las excepciones ...).

En contraste, Go y Rust adoptan el enfoque de que:

  • los errores deben señalarse en la banda,
  • La excepción debe utilizarse para situaciones realmente excepcionales.

Esto último es bastante evidente en que (1) nombran sus excepciones en pánico y (2) no hay una jerarquía de tipos / cláusula complicada aquí. El lenguaje no ofrece facilidades para inspeccionar el contenido de un "pánico": sin jerarquía de tipos, sin contenido definido por el usuario, solo "oops, las cosas salieron tan mal que no hay recuperación posible".

Esto efectivamente alienta a los usuarios a usar el manejo adecuado de errores, al tiempo que deja una forma fácil de rescatar en situaciones excepcionales (como: "¡espera, aún no lo he implementado!").

Por supuesto, el enfoque Ir desafortunadamente es muy similar al suyo, ya que puede olvidarse fácilmente de verificar el error ...

... sin embargo, el enfoque de Rust se centra principalmente en dos tipos:

  • Option, que es similar a std::optional,
  • Result, que es una variante de dos posibilidades: Ok y Err.

esto es mucho más ordenado porque no hay oportunidad de usar accidentalmente un resultado sin haber verificado el éxito: si lo hace, el programa entra en pánico.


Los lenguajes FP forman su manejo de errores en construcciones que se pueden dividir en tres capas: - Functor - Aplicativo / Alternativo - Mónadas / Alternativa

Echemos un vistazo a la Functorclase de tipo de Haskell :

class Functor m where
  fmap :: (a -> b) -> m a -> m b

En primer lugar, las clases de tipos son algo similares pero no iguales a las interfaces. Las firmas de funciones de Haskell dan un poco de miedo en un primer vistazo. Pero descifrémoslos. La función fmaptoma una función como primer parámetro que es algo similar a std::function<a,b>. Lo siguiente es un m a. Puedes imaginarte mcomo algo así std::vectory m acomo algo así std::vector<a>. Pero la diferencia es que eso m ano dice que tiene que ser explícitamente std:vector. Por lo tanto, también podría ser un std::option. Al decirle al idioma que tenemos una instancia para la clase Functorde tipos para un tipo específico como std::vectoro std::option, podemos usar la función fmappara ese tipo. Lo mismo debe hacerse para las clases de tipos Applicative,Alternative yMonadque le permite hacer cálculos con estado y posibles fallas. La Alternativeclase de tipos implementa abstracciones de recuperación de errores. Por eso puedes decir algo así como a <|> bque es término ao término b. Si ninguno de los dos cálculos tiene éxito, sigue siendo un error.

Echemos un vistazo al Maybetipo de Haskell .

data Maybe a
  = Nothing
  | Just a

Esto significa que donde esperas un Maybe a, obtienes uno Nothingo Just a. Al mirar fmapdesde arriba, una implementación podría verse como

fmap f m = case m of
  Nothing -> Nothing
  Just a -> Just (f a)

La case ... ofexpresión se llama coincidencia de patrones y se asemeja a lo que se conoce en el mundo OOP visitor pattern. Imagine la línea case m ofcomo m.apply(...)y los puntos es la instanciación de una clase que implementa las funciones de envío. Las líneas debajo de la case ... ofexpresión son las funciones de despacho respectivas que llevan los campos de la clase directamente al alcance por nombre. En la Nothingrama que creamos Nothingy en la Just arama nombramos nuestro único valor ay creamos otro Just ...con la función de transformación faplicada a. Leerlo como: new Just(f(a)).

Esto ahora puede manejar cálculos erróneos mientras abstrae las comprobaciones de error reales. Existen implementaciones para las otras interfaces que hacen que este tipo de cálculos sea muy poderoso. En realidad, Maybees la inspiración para Rust's Option-Type.


En ese caso, te animo a que vuelvas a trabajar en tu Successclase hacia una Result. Alexandrescu en realidad propuso algo realmente cercano, llamado expected<T>, para lo cual se hicieron propuestas estándar .

Me atendré a los nombres y API de Rust simplemente porque ... está documentado y funciona. Por supuesto, Rust tiene un ingenioso ?operador de sufijo que haría el código mucho más dulce; en C ++, usaremos la expresión deTRY macro y declaraciones de GCC para emularlo.

template <typename E>
struct Error {
    Error(E e): error(std::move(e)) {}

    E error;
};

template <typename E>
Error<E> error(E e) { return Error<E>(std::move(e)); }

template <typename T, typename E>
struct [[nodiscard]] Result {
    template <typename U>
    Result(U u): ok(true), data(std::move(u)), error() {}

    template <typename F>
    Result(Error<F> f): ok(false), data(), error(std::move(f.error)) {}

    template <typename U, typename F>
    Result(Result<U, F> other):
        ok(other.ok), data(std::move(other.data)),  error(std::move(other.error)) {}

    bool ok = false;
    T data;
    E error;
};

#define TRY(Expr_) \
    ({ auto result = (Expr_); \
       if (!result.ok) { return result; } \
       std::move(result.data); })

Nota: este Resultes un marcador de posición. Una implementación adecuada usaría encapsulación y a union. Sin embargo, es suficiente para transmitir el punto.

Lo que me permite escribir ( verlo en acción ):

Result<double, std::string> sqrt(double x) {
    if (x < 0) {
        return error("sqrt does not accept negative numbers");
    }
    return x;
}

Result<double, std::string> double_sqrt(double x) {
    auto y = TRY(sqrt(x));
    return sqrt(y);
}

lo cual me parece realmente genial:

  • a diferencia del uso de códigos de error (o de su Successclase), olvidarse de buscar errores dará como resultado un error de tiempo de ejecución 1 en lugar de un comportamiento aleatorio,
  • a diferencia del uso de excepciones, es evidente en el sitio de la llamada qué funciones pueden fallar, por lo que no sorprende.
  • con C ++ - 2X estándar, podemos entrar conceptsen el estándar. Esto haría que este tipo de programación sea mucho más placentera, ya que podríamos elegir el tipo de error. Por ejemplo, con una implementación de std::vectorcomo resultado, podríamos calcular todas las soluciones posibles a la vez. O podríamos elegir mejorar el manejo de errores, como usted propuso.

1 Con una Resultimplementación adecuadamente encapsulada ;)


Nota: a diferencia de la excepción, este peso ligero Resultno tiene retrocesos, lo que hace que el registro sea menos eficiente; puede resultarle útil registrar al menos el número de archivo / línea en el que se genera el mensaje de error y, en general, escribir un mensaje de error enriquecido. Esto se puede complicar capturando el archivo / línea cada vez que TRYse usa la macro, esencialmente creando libbacktracela traza inversa manualmente, o usando código específico de la plataforma y bibliotecas como para enumerar los símbolos en la pila de llamadas.


Sin embargo, hay una gran advertencia: las bibliotecas C ++ existentes, e incluso std, se basan en excepciones. Usar este estilo será una batalla cuesta arriba, ya que la API de cualquier biblioteca de terceros debe estar envuelta en un adaptador ...

Matthieu M.
fuente
3
Esa macro se ve ... muy mal. Supongo que ({...})es una extensión de gcc, pero aun así, ¿no debería ser así if (!result.ok) return result;? Su condición aparece al revés y hace una copia innecesaria del error.
Mooing Duck
@MooingDuck La respuesta explica que ({...})es la expresión de las declaraciones de gcc .
jamesdlin
1
Recomiendo usar std::variantpara implementar el Resultsi está usando C ++ 17. Además, para recibir una advertencia si ignora un error, use[[nodiscard]]
Justin
2
@Justin: Si usarlo std::varianto no es algo de gusto, dadas las compensaciones en torno al manejo de excepciones. [[nodiscard]]es de hecho una victoria pura.
Matthieu M.
46

RECLAMACIÓN: El mecanismo de excepción es un lenguaje semántico para manejar errores

Las excepciones son un mecanismo de control de flujo. La motivación para este mecanismo de control de flujo fue separar específicamente el manejo de errores del código de manejo sin errores, en el caso común de que el manejo de errores es muy repetitivo y tiene poca relevancia para la parte principal de la lógica.

RECLAMACIÓN: Para mí, no hay "excusa" para que una función no logre una tarea: o definimos erróneamente las condiciones previas / posteriores para que la función no pueda garantizar resultados, o algún caso excepcional específico no se considera lo suficientemente importante como para pasar tiempo en el desarrollo una solución

Considere: trato de crear un archivo. El dispositivo de almacenamiento está lleno.

Ahora, esto no es un fracaso para definir mis condiciones previas: no se puede usar "debe haber suficiente almacenamiento" como condición previa en general, porque el almacenamiento compartido está sujeto a condiciones de carrera que hacen que esto sea imposible de satisfacer.

Entonces, ¿debería mi programa liberar algo de espacio y luego continuar con éxito, de lo contrario, soy demasiado vago para "desarrollar una solución"? Esto parece francamente sin sentido. La "solución" para la gestión del almacenamiento compartido es fuera del alcance de mi programa , y permitiendo que mi programa falle con gracia, y volver a efectuar una vez que el usuario ha lanzado ya sea un poco de espacio, o añadido algo más capacidad de almacenamiento, es fina .


Lo que hace su clase de éxito es intercalar el manejo de errores de manera muy explícita con la lógica de su programa. Cada función debe verificar, antes de ejecutarse, si ya se produjo algún error, lo que significa que no debería hacer nada. Cada función de la biblioteca debe estar envuelta en otra función, con un argumento más (y con suerte un reenvío perfecto), que hace exactamente lo mismo.

Tenga en cuenta también que su mySqrtfunción necesita devolver un valor incluso si falló (o si una función anterior había fallado). Por lo tanto, está devolviendo un valor mágico (como NaN), o inyectando un valor indeterminado en su programa y esperando que nada lo use sin verificar el estado de éxito que ha introducido durante su ejecución.

Para la corrección, y el rendimiento, es mucho mejor volver a pasar el control fuera del alcance una vez que no pueda avanzar. Las excepciones y la comprobación explícita de errores de estilo C con retorno temprano logran esto.


A modo de comparación, un ejemplo de su idea que realmente funciona es la mónada Error en Haskell. La ventaja sobre su sistema es que normalmente escribe la mayor parte de su lógica y luego la envuelve en la mónada, que se encarga de detener la evaluación cuando falla un paso. De esta forma, el único código que toca directamente el sistema de manejo de errores es el código que podría fallar (arrojar un error) y el código que necesita para hacer frente a la falla (detectar una excepción).

Sin embargo, no estoy seguro de que el estilo de mónada y la evaluación perezosa se traduzcan bien a C ++.

Inútil
fuente
1
Gracias a su respuesta, agrega luz al tema. Supongo que el usuario no estaría de acuerdo con and allowing my program to fail gracefully, and be re-runcuando acaba de perder 2 horas de trabajo:
Adrian Maire
14
Su solución significa que en cada lugar donde pueda crear un archivo, debe solicitar al usuario que solucione la situación y vuelva a intentarlo. Luego, cualquier otra cosa que pueda salir mal, también debe solucionarlo localmente. Con excepciones, solo se captura std::exceptionen el nivel superior de la operación lógica, le dice al usuario "X falló debido a ex.what ()" y le ofrece volver a intentar toda la operación cuando esté listo.
Inútil
13
@AdrianMaire: El "permitir fallar con gracia y volver a ejecutarse" también podría implementarse como showing the Save dialog again along with an error message and allowing the user to specify an alternative location to try. Es un manejo elegante de un problema que normalmente no se puede hacer desde el código que detecta que la primera ubicación de almacenamiento está llena.
Bart van Ingen Schenau
3
La evaluación de @Useless Lazy no tiene nada que ver con el uso de la mónada Error, como lo demuestran los lenguajes de evaluación estrictos como Rust, OCaml y F # que todos hacen un uso intensivo de la misma.
8bittree
1
@Inutilizando IMO para un software de calidad, tiene sentido que "en cada lugar donde pueda crear un archivo, necesite pedirle al usuario que solucione la situación y vuelva a intentarlo". Los primeros programadores a menudo llegaron a extremos notables hacia la recuperación de errores, al menos el programa TeX de Knuth está lleno de ellos. Y con su marco de "programación alfabetizada", encontró una manera de mantener el manejo de errores en otra sección, para que el código permanezca legible y la recuperación de errores se escriba con más cuidado (porque cuando está escribiendo la sección de recuperación de errores, ese es el punto y el programador tiende a hacer un mejor trabajo).
ShreevatsaR
15

Me gustaría entender mejor las implicaciones de usar tal paradigma en un proyecto:

  • ¿Es correcta la premisa del problema? o ¿Me perdí algo relevante?
  • ¿Es la solución una buena idea arquitectónica? o el precio es demasiado alto?

Su enfoque trae algunos problemas importantes a su código fuente:

  • se basa en el código del cliente siempre recordando verificar el valor de s. Esto es común con el uso de códigos de retorno para el enfoque de manejo de errores , y una de las razones por las que se introdujeron excepciones en el lenguaje: con excepciones, si falla, no falla en silencio.

  • cuanto más código escriba con este enfoque, más código de error tendrá que agregar también, para el manejo de errores (su código ya no es minimalista) y su esfuerzo de mantenimiento aumentará.

Pero mis varios años como desarrollador me hacen ver el problema desde un enfoque diferente:

Las soluciones para estos problemas deben abordarse a nivel técnico o de equipo:

Los programadores tienden a simplificar su tarea lanzando excepciones cuando el caso específico parece demasiado raro para implementarlo con cuidado. Los casos típicos de esto son: problemas de falta de memoria, problemas de disco lleno, problemas de archivos corruptos, etc. Esto puede ser suficiente, pero no siempre se decide desde un nivel arquitectónico.

Si te encuentras manejando todo tipo de excepción que pueda ser lanzada, todo el tiempo, entonces el diseño no es bueno; Los errores que se manejan deben decidirse de acuerdo con las especificaciones del proyecto, no de acuerdo con lo que los desarrolladores desean implementar.

Aborde mediante la configuración de pruebas automatizadas, separando la especificación de las pruebas unitarias y la implementación (haga que dos personas diferentes hagan esto).

Los programadores tienden a no leer cuidadosamente la documentación [...] Además, incluso cuando saben, realmente no los manejan.

No abordará esto escribiendo más código. Creo que su mejor opción son las revisiones de código aplicadas meticulosamente.

Los programadores tienden a no detectar excepciones lo suficientemente temprano, y cuando lo hacen, es principalmente para iniciar sesión y lanzar más. (consulte el primer punto).

El manejo adecuado de errores es difícil, pero menos tedioso con excepciones que con valores de retorno (ya sea que se devuelvan o se pasen como argumentos de E / S).

La parte más complicada del manejo de errores no es cómo recibe el error, sino cómo asegurarse de que su aplicación mantenga un estado consistente en presencia de errores.

Para abordar esto, se debe asignar más atención a la identificación y ejecución en condiciones de error (más pruebas, más pruebas de unidad / integración, etc.).

utnapistim
fuente
12
Se omite todo el código después de un error, si recuerda comprobar cada vez que recibe una instancia como argumento . Esto es lo que quise decir con "cuanto más código escriba con este enfoque, más código de error tendrá que agregar también". Tendrá que descifrar su código con ifs en la instancia de éxito, y cada vez que lo olvide, es un error. El segundo problema causado por olvidar verificar: el código que se ejecuta hasta que vuelva a verificar, no debería haberse ejecutado en absoluto (continuar si olvida verificar, corrompe sus datos).
utnapistim
11
No, manejar una excepción (o devolver un código de error) no es un bloqueo, a menos que el error / excepción sea lógicamente fatal o elija no manejarlo. Todavía tiene la oportunidad de manejar el caso de error, sin tener que verificar explícitamente en cada paso si se produjo un error anteriormente
Inútil
11
@AdrianMaire En casi todas las aplicaciones en las que trabajo, preferiría un bloqueo en lugar de continuar en silencio. Trabajo en un software crítico para el negocio en el que tomar algunos resultados malos y continuar operando en él podría resultar en una gran pérdida de dinero. Si la corrección es crucial y el bloqueo aceptable, entonces las excepciones tienen una gran ventaja aquí.
Chris Hayes
1
@AdrianMaire: creo que es mucho más difícil olvidar manejar una excepción que su método de olvidar una declaración if ... Además, el principal beneficio de las excepciones es qué capa las maneja. Es posible que desee que una excepción del sistema aparezca más para mostrar un mensaje de error de nivel de aplicación, pero maneje situaciones que conoce en un nivel inferior. Si está utilizando bibliotecas de terceros u otro código de desarrolladores, esta es realmente la única opción ...
Milney
55
@ Adrian No hay error, parece que has leído mal lo que escribí o te perdiste la segunda mitad. Mi punto principal no es que la excepción se lanzará durante las pruebas / desarrollo y que los desarrolladores se darán cuenta de que necesitan manejarlos. El punto es que la consecuencia de una excepción completamente no controlada en la producción es preferible a la consecuencia de un código de error no verificado. Si pierde el código de error, obtendrá y continuará utilizando resultados incorrectos. Si omite la excepción, la aplicación se bloquea y no continúa ejecutándose, no obtendrá resultados ni resultados incorrectos . (cont.)
Mr.Mindor