Exceptions vs error codes

Судя по всему, неделя исключений на хабре успешно наступила :). Накопив достаточную «подушку» кармы чтобы не бояться уйти в минус, я, пожалуй, тоже выскажу свое...

Судя по всему, неделя исключений на хабре успешно наступила :). Накопив достаточную «подушку» кармы чтобы не бояться уйти в минус, я, пожалуй, тоже выскажу свое мнение по данному вопросу. Сразу оговорюсь, что мнение сугубо личное, основанное на небольшом практическом опыте коммерческой разработки: C++, Objective-C, C#, Java, Python, Ruby.

Что такое ошибка?

Прежде чем приступать к кровавому месиву, желательно оценить поле боя и синхронизировать терминологию. Под «ошибками» в контексте разработки программного обеспечения можно понимать достаточно разные вещи, поэтому для начала я попробую эти сущности как-то обрисовать и обозвать:

  1. Самое простое что в программе может случиться — это сбой операционной системы или железа. Не сработавший системный вызов CreateEvent() или pthread_mutex_lock(), деление на ноль, мусор в результатах системного вызова — все это может случиться по разным причинам, начиная от вышедшего из строя железа и заканчивая вирусами в системе, но как правило от нас и нашей программы это не очень зависит.
  2. Чуть более сложная ситуация — это отсутствие нужных нам ресурсов. Неожиданно может закончиться память, хэндлы, файловые дескрипторы. Может не быть прав на запись или чтение нужных файлов. Пайп может не открыться. Или наоборот — не закрыться. Доступ к базе данных может быть — а может и не быть. Такая ситуация уже может быть вызвана как нашей программой (слишком много памяти восхотелось) так и нестабильностью системы (вирусу слишком много памяти восхотелось).
  3. А самая распространенная ситуация — это ошибка в логике программы или взаимодействия ее частей. Мы пытаемся удалить несуществующий элемент списка, вызывать метод с неверными аргументами, многопоточно выполнить неатомарную операцию. Как правило это приводит или к некорректному поведению программы («тулбар исчез») или к ее краху с access violation / unhandled exception.

Как видите, много всего разного и нехорошего может произойти — а ведь это далеко не полный список :). А что делать программисту? Тут, на мой взгляд, перед нами встает очень интересный и важный вопрос — как именно нашей программе реагировать на ту или иную ошибку? Пожалуй сейчас я еще раз напомню, что излагаю свое сугубо личное мнение. И скажу следующее — как именно реагировать на ошибку целиком зависит от конкретной программы. Если у нас закончилась память в драйвере — мы должны любой ценой выжить, чтобы пользователь не получил синего экрана смерти. Если же у нас закончилась память в игрушке типа веселая ферма — то имеет смысл упасть, извиниться и попросить отправить багрепорт разработчику. Системный сервис, призванный крутиться многие месяцы без перезагрузки, должен с пониманием отнестись к ошибке CreateEvent(). Та жа ошибка в прикладной программе типа Photoshop означает что скорее всего система через секунду умрет, и лучше честно упасть, нежели попытаться проглотить ошибку, дать пользователю сохранить файл и благополучно его испортить из-за последующего сбоя во время записи. Следовательно ошибки мы можем делить на ожидаемые и неожиданные. Для разных программ и разных требований одни и те же ошибки могут считаться как ожидаемыми, так и неожиданными. С ожидаемыми ошибками мы как-то работаем. Не получилось открыть файл — говорим об этом пользователю и продолжаем работу. Не удалось выделить память для загрузки туда гигабайтного файла — говорим об этом пользователю и продолжаем работу. С неожиданными ошибками мы в большинстве случаев не работаем. Закончилась память при попытке выделить двадцать байт для создания объекта — падаем. Не создался системный объект которых на всю программу три штуки — падаем. Не читается системный пайп который по спецификации должен читаться? Лучше упасть, чем оставить программу в нестабильном состоянии и потом испортить пользователю данные. Программу он если что перезапустит, а вот за испорченный файл возненавидит до конца дней своих. А для серьезных случаев есть автосейв и перезапускающий нас ежели чего watchdog.

Что было до исключений?

В эпоху расцвета процедурного программирования синтаксис работы с ошибками был тривиален и основывался на том, что вернула функция. Если функция возвращала TRUE — все хорошо, если же FALSE — то произошла ошибка. При этом сразу выделились два подхода к работе с ошибками:

  • Подход два в одном — функция возвращает FALSE или нулевой указатель как для ожидаемой, так и для неожиданной ошибки. Такой подход как правило применялся в API общего назначения и коде пользовательских программ, когда большую часть ошибок можно было смело считать фатальными и падать. Для тех редких случаев когда делить было все же нужно использовалась некая дополнительная машинерия вида GetLastError(). Фрагмент кода того времени, копирующего данные из одного файла в другой и возвращающего ошибку в случае возникновения любых проблем:
    BOOL Copy( CHAR* sname, CHAR* dname )
    {
      FILE *sfile = 0, *dfile = 0;
      void* mem = 0;
      UINT32 size = 0, written = 0;
      BOOL ret = FALSE;
    
      sfile = fopen( sname, "rb" );
      if( ! sfile ) goto cleanup;
      dfile = fopen( dname, "wb" );
      if( ! dfile ) goto cleanup;
      mem = malloc( F_CHUNK_SIZE );
      if( ! mem ) goto cleanup;
      do
      {
        size = fread( sfile, mem, F_CHUNK_SIZE );
        written = fwrite( dfile, mem, size );
        if( size != written ) goto cleanup;
      }
      while( size )
      ret = TRUE;
    cleanup: // Аналог деструктора.
      if( sfile) fclose( sfile );
      if( dfile) fclose( dfile );
      if( mem ) free( mem );
      return ret; // Ожидаемая ошибка.
    }
    

  • Подход разделения ошибок, при котором функция возвращает FALSE в случае неожиданной ошибки, а ожидаемую ошибку возвращает отдельным возвращаемым значением (в примере это error), если нужно. Такой подход применялся в более надежном коде, например apache, и подразумевал разделение на ожидаемые ошибки (файл не получилось открыть потому что его нет) и неожиданные (файл не получилось открыть потому, что закончилась память и не получилось выделить 20 байт чтобы скопировать строку с именем). Фрагмент того же код, но уже разделяющего неожиданную ошибку (возврат FALSE) и ожидаемую (возврат HANDLE).
    BOOL Copy( CHAR* sname, CHAR* dname, OUT HANDLE* error )
    {
      HANDLE sfile = 0, dfile = 0, data = 0;
      UINT32 size = 0;
    
      ENSURE( PoolAlloc() ); // Макрос обеспечивает обработку неожиданной ошибки.
      ENSURE( FileOpen( sname, OUT& sfile, OUT error ) );
      REQUIRE( SUCCESS( error ) ); // Макрос обеспечивает обработку ожидаемой ошибки.
      ENSURE( FileOpen( dname, OUT& dfile, OUT error ) );
      REQUIRE( SUCCESS( error ) );
      ENSURE( MemAlloc( OUT& data ) );
      REQUIRE( SUCCESS( error ) );
      do
      {
        ENSURE( FileRead( sfile, F_CHUNK_SIZE, OUT& data, OUT error ) );
        REQUIRE( SUCCESS( error ) );
        ENSURE( FileWrite( dfile, & data ) );
        REQUIRE( SUCCESS( error ) );
        ENSURE( MemGetSize( OUT& size ) )
      }
      while( size );
      ENSURE( PoolFree() ); // Пул обеспечивает аналог деструкторов и RAII.
      return TRUE;
    }
    

Через некоторое время разработчики заметили, что большинство успешных решений использует ООП и решили что неплохо бы его вынести в синтаксис языка, дабы писать больше кода по делу и меньше — повторяющегося кода для поддержки архитектуры.

Что стало после введения исключений

Давайте возьмем код выше и посмотрим, как он трансформировался после добавления ООП в синтаксис языков программирования. Конструирование и уничтожение объектов (fopen, fclose) стало конструкторами и деструкторами. Переброс неожиданной ошибки (BOOL ret в первом примере, макрос ENSURE во втором) однозначно стал исключением.

А вот с ожидаемой ошибкой случилось самое интересное — случился выбор. Можно было использовать возвращаемое значение — теперь, когда заботу о неожиданных ошибках взяли на себя исключения, возвращаемое значение снова стало в полном распоряжении программиста. А можно было использовать исключения другого типа — если функции копирования файлов самой не нужно обрабатывать ожидаемые ошибки то логично вместо if и REQUIRE просто ничего не делать — и оба типа ошибок уйдут вверх по стеку. Соответственно, у программистов снова получилось два варианта:

  • Подход только исключения — ожидаемые и неожиданные ошибки — это разные типы исключений.
    void Copy( string sname, string dname )
    {
      file source( sname );
      file destination( sname );
      source.open( "rb" );
      destination.open( "wb" );
      data bytes;
      do
      {
        bytes = source.read( F_CHUNK_SIZE );
        destination.write( bytes )
      }
      while( bytes.size() )
    }
    

  • Комбинированный подход — использование исключений для неожиданных ошибок и кодов возврата / nullable типов для ожидаемых:
    bool Copy( string sname, string dname )
    {
      file source( sname );
      file destination( sname );
      if( ! source.open( "rb" ) || ! destination.open( "wb" ) ) return false;
      data bytes;
      do
      {
        bytes = source.read( F_CHUNK_SIZE );
        if( bytes.isValid() )
        {
          if( ! destination.write( bytes ) ) return false;
        }
      }
      while( bytes.isValid() && bytes.size() )
    }
    

Почему выжили коды возврата?

Здесь я еще раз напомню, что высказываю свое личное мнение и открыт к обсуждению :). Итак, если внимательно посмотреть на два приведенных выше фрагмента кода то становится не совсем понятно почему выжил второй. Кода в нем объективно больше. Выглядит менее красиво. Если функция возвращает объект — то использовать коды возврата совсем неудобно. Вопрос — почему коды возврата вообще выжили в языках с поддержкой объектно-ориентированного программирования и исключений на уровне синтаксиса? Что я могу по этому поводу сказать:

  • Первые реализации исключений, особенно в C++, были не очень удобны для ежедневного использования. Например, бросание исключения во время обработки другого исключения приводил к завершению программы. Или же бросание исключения в конструкторе приводило к тому, что деструктор не вызывался.
  • Разработчикам API забыли объяснить для чего нужны исключения. В результате первое время не было даже деления на ожидаемые (checked) и неожиданные (unchecked), а API комбинировали как исключения, так и коды возврата.
  • В большинстве языков для исключений забыли добавить семантику «игнорировать ожидаемую ошибку». В результате на практике код, использующий исключения как для ожидаемых так и для неожиданных ошибок, с невероятной скоростью обрастал try и catch везде, где только можно.

Выводы

Что бы я хотел резюмировать. На мой взгляд, большинство проблем с исключениями были вызваны первыми, не очень удачными реализациями — особенно в C++. Так что выбор «использовать только коды возврата», «использовать исключения для неожиданных ошибок и коды возврата для ожидаемых» или «использовать исключения для всего» по большей части имеется только для C++. Надеюсь, мой краткий рассказ о причинах появления исключений в современных языках программирования поможет разработчикам чуть лучше ориентироваться в современных API и замечать места, где авторы использую исключения некорректно. Понимание какие из ошибок мы считаем для нашей программы ожидаемыми, какие — неожиданные и как это оптимальным образом ложится на предоставляемую языком и API модель исключений позволяет писать простой, понятный и внимательно следящий за ошибками код.

We are building a web service(SOAP, .Net) which would be talking to (mostly) native clients (windows, C++) and we are wondering what is the best way to communicate errors to the client (e.g. SomethingBadHappened like login service not available or something like user not found) and haven’t been able to decide between throwing exception to the client or using some kind of error code model to do the above.

What you would prefer on the handling on the client side: receiving a error code or handling a ServerFault exception which contains the reason for the error?
1) Why are we thinking exception: Because it would make server side code a lot more uniform
2) Why are we thinking error codes: Because we think it makes more sense from the client side perspective.

If 2) is really true we would probably want to go for error codes than exceptions? Is that the case here?

Also, would the answer change if we were talking to managed clients instead of native clients?

gnat's user avatar

gnat

21.7k29 gold badges111 silver badges285 bronze badges

asked Nov 1, 2010 at 4:38

Amit Wadhwa's user avatar

2

SOAP has a concept of faults, you can convert an exception to a fault on the server side and on the client proxy the fault can again be converted back to an exception. This works remarkably well in WCF and Java metro stack, cannot comment on native C++ clients.

As regards to SOA best practice define one generic fault and few specific faults only if the client need to handle a certain type of error differently. Never send a exception stack trace to client in production deployment. This is because in theory the server trace has no meaning for the client and for security reasons as well. Log the full error and stacktrace on the server and send a unique reference to the log in the fault. In WCF I use the Microsoft Exception Handling block from Enterprise Library to generate a guid and also convert a exception to SOAP fault.

Check the guidance at Microsoft Patterns and Practices.

answered Nov 1, 2010 at 9:24

softveda's user avatar

softvedasoftveda

2,6691 gold badge21 silver badges21 bronze badges

3

I recently did a web service with the Java 6 libraries, which can report an exception back to the caller (I haven’t looked into how as it is done automatically).

The ability to have the client provide a stack trace in the error report to the developer has been very useful (opposed to getting an approximate timestamp and then have to look it up in your logs, if you happen to log it).

So, seen from a developers point of view, use Exceptions.

answered Nov 1, 2010 at 6:41

4

If it’s a web service, you can’t exactly cause the server to throw an exception that will be caught by the client. At the interface, your server basically has to return some sort of error code, even if that’s a string that says An exception occurred. Type %s, message %s, stack trace %s.

As for the client side, you could have your response reading code check the response to see if it contains an error and raise an exception on the client side. That’s a very good way to do it, in languages with good exception handling at least. C++, though, does not have good exception handing, and it’s a good idea to stay as far away from C++ exceptions as possible. If you can’t use a better language, then just stick to error codes.

answered Nov 1, 2010 at 4:49

Mason Wheeler's user avatar

Mason WheelerMason Wheeler

81.7k24 gold badges234 silver badges309 bronze badges

4

Exceptions vs Error Codes

There’s a historical debate about best practice for error handling in imperative
code: Exceptions or Status Returns (Error Codes).

Here I present some examples as to why I believe exceptions are superior.

tl;dr: Exceptions fail loudly, and that’s a good thing.

Examples inspired by:

  • Exceptions vs. status returns
  • Exceptions in the rainforest

Proper handling

Examples of code properly handling error codes and exceptions.

properly-handle-error.php

$ php -f properly-handle-error.php
Calling with apple...
Life is good with apple.

Calling with banana...
Something abnormal happened, but we're ok.
Life is good with banana.

Calling with orange...
Whoa something bad happened, we're done.

properly-handle-exception.php

$ php -f properly-handle-exception.php
Calling with apple...
Life is good with apple.

Calling with banana...
Something abnormal happened, but we're ok.
Life is good with banana.

Calling with orange...
Whoa something bad happened, we're done.

Improper handling

Examples of code improperly handling error codes and exceptions. Notice that
when error codes are improperly handled they fail silently, while exceptions
fail loudly.

improperly-handle-error.php

$ php -f improperly-handle-error.php
Calling with apple...
Life is good with apple.

Calling with banana...
Something abnormal happened, but we're ok.
Life is good with banana.

Calling with orange...
Life is good with orange.

improperly-handle-exception.php

$ php -f improperly-handle-exception.php
Calling with apple...
Life is good with apple.

Calling with banana...
Something abnormal happened, but we're ok.
Life is good with banana.

Calling with orange...
PHP Warning:  array_map(): An error occurred while invoking the map callback in /Users/c4rl/_src/exceptionsvserrorcodes/improperly-handle-exception.php on line 32
PHP Stack trace:
PHP   1. {main}() /Users/c4rl/_src/exceptionsvserrorcodes/improperly-handle-exception.php:0
PHP   2. array_map() /Users/c4rl/_src/exceptionsvserrorcodes/improperly-handle-exception.php:32

Warning: array_map(): An error occurred while invoking the map callback in /Users/c4rl/_src/exceptionsvserrorcodes/improperly-handle-exception.php on line 32

Call Stack:
    0.0004     228992   1. {main}() /Users/c4rl/_src/exceptionsvserrorcodes/improperly-handle-exception.php:0
    0.0010     249440   2. array_map() /Users/c4rl/_src/exceptionsvserrorcodes/improperly-handle-exception.php:32

PHP Fatal error:  Uncaught exception 'FooEmergencyException' in /Users/c4rl/_src/exceptionsvserrorcodes/exception.inc:22
Stack trace:
#0 /Users/c4rl/_src/exceptionsvserrorcodes/improperly-handle-exception.php(20): foo('orange')
#1 [internal function]: {closure}('orange')
#2 /Users/c4rl/_src/exceptionsvserrorcodes/improperly-handle-exception.php(32): array_map(Object(Closure), Array)
#3 {main}
  thrown in /Users/c4rl/_src/exceptionsvserrorcodes/exception.inc on line 22

Fatal error: Uncaught exception 'FooEmergencyException' in /Users/c4rl/_src/exceptionsvserrorcodes/exception.inc on line 22

FooEmergencyException:  in /Users/c4rl/_src/exceptionsvserrorcodes/exception.inc on line 22

Call Stack:
    0.0004     228992   1. {main}() /Users/c4rl/_src/exceptionsvserrorcodes/improperly-handle-exception.php:0
    0.0010     249440   2. array_map() /Users/c4rl/_src/exceptionsvserrorcodes/improperly-handle-exception.php:32
    0.0016     250640   3. {closure:/Users/c4rl/_src/exceptionsvserrorcodes/improperly-handle-exception.php:15-32}() /Users/c4rl/_src/exceptionsvserrorcodes/improperly-handle-exception.php:32
    0.0016     250640   4. foo() /Users/c4rl/_src/exceptionsvserrorcodes/improperly-handle-exception.php:20

No handling

Examples of code without handling error codes and exceptions. Again, exceptions
fail loudly, even those exceptions that do not necessarily indicate an unstable
system (i.e. FooWarningException).

no-handle-error.php

$ php -f no-handle-error.php
Calling with apple...
Life is good with apple.

Calling with banana...
Life is good with banana.

Calling with orange...
Life is good with orange.

no-handle-exception.php

$ php -f no-handle-exception.php
Calling with apple...
Life is good with apple.

Calling with banana...
PHP Warning:  array_map(): An error occurred while invoking the map callback in /Users/c4rl/_src/exceptionsvserrorcodes/no-handle-exception.php on line 23
PHP Stack trace:
PHP   1. {main}() /Users/c4rl/_src/exceptionsvserrorcodes/no-handle-exception.php:0
PHP   2. array_map() /Users/c4rl/_src/exceptionsvserrorcodes/no-handle-exception.php:23

Warning: array_map(): An error occurred while invoking the map callback in /Users/c4rl/_src/exceptionsvserrorcodes/no-handle-exception.php on line 23

Call Stack:
    0.0002     228096   1. {main}() /Users/c4rl/_src/exceptionsvserrorcodes/no-handle-exception.php:0
    0.0008     248528   2. array_map() /Users/c4rl/_src/exceptionsvserrorcodes/no-handle-exception.php:23

PHP Fatal error:  Uncaught exception 'FooWarningException' in /Users/c4rl/_src/exceptionsvserrorcodes/exception.inc:19
Stack trace:
#0 /Users/c4rl/_src/exceptionsvserrorcodes/no-handle-exception.php(19): foo('banana')
#1 [internal function]: {closure}('banana')
#2 /Users/c4rl/_src/exceptionsvserrorcodes/no-handle-exception.php(23): array_map(Object(Closure), Array)
#3 {main}
  thrown in /Users/c4rl/_src/exceptionsvserrorcodes/exception.inc on line 19

Fatal error: Uncaught exception 'FooWarningException' in /Users/c4rl/_src/exceptionsvserrorcodes/exception.inc on line 19

FooWarningException:  in /Users/c4rl/_src/exceptionsvserrorcodes/exception.inc on line 19

Call Stack:
    0.0002     228096   1. {main}() /Users/c4rl/_src/exceptionsvserrorcodes/no-handle-exception.php:0
    0.0008     248528   2. array_map() /Users/c4rl/_src/exceptionsvserrorcodes/no-handle-exception.php:23
    0.0009     249584   3. {closure:/Users/c4rl/_src/exceptionsvserrorcodes/no-handle-exception.php:15-23}() /Users/c4rl/_src/exceptionsvserrorcodes/no-handle-exception.php:23
    0.0009     249584   4. foo() /Users/c4rl/_src/exceptionsvserrorcodes/no-handle-exception.php:19

Error codes or exceptions – which is better? Here’s my answer:

  1. They have the same worst case – a human error can lead to a complete disaster.
  2. Exceptions are far safer for most code.
  3. Error codes are far safer for well-reviewed, critical code.

(As you can see from 2 and 3, I believe that most code is not critical and/or poorly reviewed; I think most people will agree on that one.)

Worst case: disaster

Here’s a disaster with error codes (based on an example from a critique of Go’s error handling):

seal_presidential_bunker()
trigger_doomsday_device()

If we fail to seal_presidential_bunker, we still trigger_doomsday_device, because the programmer forgot to check the error code. Human error has lead to a disaster.

(The original article doesn’t specify exactly what the disaster is. One problem is that the presidential staff is not safe. Another problem is that the doomsday device got triggered – which wouldn’t happen if an exception were thrown and left uncaught. Which of the two problems is the bigger part of the disaster depends on your worldview.)

Here’s a disaster with exceptions.

open_the_gate()
wait_for_our_men_to_come_in()
close_the_gate()

If wait_for_our_men_to_come_in throws an exception, then we’ll never close_the_gate, and the enemy will sneak in. Again – human error, disaster.

So in theory, exceptions and error codes are equally bad.

Exceptions are safer for most code

Most code doesn’t trigger doomsday devices, nor deals with lethal enemies at the gates. When most code messes up, garbage appears on the screen or in log files, and a programmer shows up to debug the problem.

With exceptions, it’s easier for the programmer to figure out why this garbage appeared, because the failure occurs closer to the point of the error.

f=open_users_file()
print_users_list(f)

If open_users_file() throws an exception, then the programmer will see a «No such file or directory» with a call stack and think, «why couldn’t this idiot [possibly, me] bother to check if the file is there?» Then he fixes the bug and all is well again.

If open_users_file() returns an invalid file object (similarly to, for example, C++’s ifstream), then print_users_list (which doesn’t check errors, either) might print an empty user list. The error might then become «No such user», or «Permission denied», etc. The program will fail further from the point of error – the file opening code – and you’ll need to go back and figure out where the error is.

For production code, failing early isn’t necessarily better. Failing early is what leaves the gate open for the enemies in the above example. Failing early due to a floating point error – instead of trying further just in case – was reportedly the root cause of the explosion of Ariane 5, costing $.5G.

But for most code, which:

  • doesn’t lead to such large damages
  • is written rather hastily
  • is expected to have bugs
  • has programmers constantly attending to it and fixing those bugs

…for most code, failing early is better simply because it always makes debugging easier – even if it doesn’t make the impact of the error smaller.

Error codes have another horrible anti-debugging quality: loss of information. Even if the program fails early with error codes, you usually only get the code of the topmost layer without all the details from below.

With an exception, you get a call stack, and an error string from the bottom layer. With a perror(), you get just an error string from the bottom layer («No such file or directory» – which file? Who wants it?). With error codes, you get something like «User list management error» – the fact that it was a file opening error gets «swallowed» by layers of code converting low-level error codes to high-level ones.

It’s possible to collect an «error code call stack» with all the information, but it’s almost never done. Whereas an exception does it automatically for the laziest of programmers. Another win for whoever gets to debug the code.

Error codes are safer for well-reviewed code

Code reviews are generally easier with error codes than exceptions. Error codes mean that you must carefully look at function calls to see if the programmer handled the possible errors. Exceptions mean that you must imagine what happens if an exception is thrown anywhere in the flow.

Making sure that the opening of every gate is exception-safe – that the gate gets closed when an exception is thrown – is hard. C++ has RAII for the gate-closing (and Python has with and C# has using), and Java has checked exceptions for the exception-hunting.

But even if you have both and then some, it still seems hard. A program has a lot of intermediate states, and some of them don’t make sense. An exception can leave you in this intermediate state. And it’s not easy to wrap every entrance into an intermediate state using whatever exception-safety-wrapper your language gives you.

So I think it makes sense for Go – a language for writing critical production code – to shun exceptions. We use C++ with -fno-exceptions for serious production code and I think it’s equally sensible.

It just doesn’t make sense to write most of your code that way. In most of my code, I want to always fail early to make debugging easier, seeing the full context of the error, and I want that to happen without putting much thought into error handling.

And this is why I think exceptions should be embraced by all lazy programmers writing low-quality code like myself.

Suppose you have a function like this:

1
2
3
4
5
6
7
/*...*/ implementation_function(const std::vector<parameter> &params){
    //...
    check_parameters(params)
    //...
    do_it(params)
    //...
}

that implements some built-in function in a language.
How would you implement error handling for this function?
1. The function handles no errors. check_parameters() and do_it() throw exceptions caught in the main loop.
2. check_parameters() and do_it() return error codes which the function returns.
3. check_parameters throws exceptions caught in the main loop, but do_it() returns error codes.
4. Other.

Note: This is a highly abstracted example. Neither check_parameters() nor do_it() exist as such. There’s a hundred other functions like implementation_function() and they all have their own unique checks and do’s to perform, thus something like

1
2
if (failed(error = check_parameters(params)))
    return error;

doesn’t work. It’s more like

1
2
3
4
5
6
7
if (params.size() != k)
    return /*...*/;
if (params[0].type() != INTEGER)
    return /*...*/;
if (!param[0].is_variable())
    return /*...*/;
//... 

I come from a Java standpoint and would prefer exceptions as opposed to error codes. Both can be well documented, but exceptions are objects that can pass a message as opposed to just a number. Error codes pass the number and then can be sent to a lookup function to get more information. I think it just comes down to personal preference. The Windows API would be an example for error codes.
So to answer your question… I would choose 4. check_parameters() and do_it() throw exceptions, but the function solves the problem if it can otherwise it passes the problem up further by throwing an exception.

but the function solves the problem if it can otherwise it passes the problem up further by throwing an exception

This has all the disadvantages of exception handling plus all the disadvantages of error codes. If the exception is going to be caught as soon as possible, then it’s pointless. You may as well simply make an Error class that contains extended error information and return that.
The point of using exceptions is that they can redirect control by unwinding the stack without any leaks, letting you write your code for the «happy path» and leave error handling for the compiler.
If your code throws an exception, there shouldn’t be anything that can be fixed. The program state (or a subset thereof) is beyond all hope and the control flow needs to leave as soon as possible before it starts to smell.

Here are some «wrong exception-handling mindsets» in no apparent order:
* The Java mindset: In Java, non-memory resources are reclaimed via explicit try/finally blocks. When this mindset is used in C++, it results in a large number of unnecessary try blocks, which, compared with RAII, clutters the code and makes the logic harder to follow. Essentially the code swaps back and forth between the «good path» and the «bad path» (the latter meaning the path taken during an exception). With RAII, the code is mostly optimistic — it’s all the «good path,» and the cleanup code is buried in destructors of the resource-owning objects. This also helps reduce the cost of code reviews and unit-testing, since these «resource-owning objects» can be validated in isolation (with explicit try/catch blocks, each copy must be unit-tested and inspected individually; they cannot be handled as a group).

http://www.parashift.com/c++-faq/mindset-for-proper-use-of-eh.html

Catching exceptions in the implementation functions is simply not acceptable, or useful for that matter.

Was it too much to say if it can? For example a simple failed HTTP request could be retried? Or maybe an SMTP request can look for a backup MX server? Almost all exceptions should be sent to the caller, meaning no try catch should be in the implementation function if no easy fix can be applied. Personally I do not clutter my code with try blocks in C++, but my main function usually does a lot of try catch stuff. Most people, including me, would say to just get the heck out of dodge. I switched to C++ because I was tired of all the memory issues in Java. Java makes code less elegant and more cluttered with memory fixing code, not to mention a lot more try catch blocks than what really seems necessary. I still prefer exceptions because I am too lazy to deal with looking up error codes or writing a lookup function for custom error codes. I really do not see a reason for custom error code functions when I can just use the STL exception classes with specific messages.

There is not really a catch all answer to your question. I think it would really depend on the system and what it will be used for. All that being said, you did ask an opinion based question. My opinion is still choice 4.

And I am curious as to why you said…

plus all the disadvantages of error codes

I just read this
http://www.joelonsoftware.com/articles/Wrong.html

And I have to say there are some very good arguments against exceptions in this. I would change my opinion to choice 2, for systems that need to be reliable. I agree with the author on using exceptions for small projects.

Last edited on

I don’t like exceptions in any languages…so, yeah, that’s my stance. Error codes are also ugly, though… :(

For example a simple failed HTTP request could be retried? Or maybe an SMTP request can look for a backup MX server?

Those are good examples of code that could both throw and return error codes on different situations. It’s not the same when a GET fails because of a transport error (e.g. the network is down) as when it reaches the server but it returns 404.

Now, imagine a function that opens a file in a dynamic language: open(). If «hello.txt» doesn’t exist, is the error caused by open("hello.txt") of the same criticality as the error caused by open(42)? I think not, but from a point of view of pure design (this was my real question. Sorry about not making it clearer):

Which is better? A function that throws on all errors, or a function that throws on some errors and returns on other errors?

I think there’s compelling arguments for both. Going back to the open() example, it’s hard to tell whether open("hello.txt") actually failed. On the one hand, it tells the user whether «hello.txt» exists, so maybe it depends on the intent of the caller. On the other, the file is still not open after the return. Maybe it warrants a NotQuiteSuccessException, or FailureWithPostconditionsMetException?

It should be the constructor for or member function in a class, and you can check the state of the class afterwards.

Exceptions are simply a way to indicate failed preconditions (and invariants, but this thread is about preconditions). The other ways are termination and documented undefined behavior, which are both useful techniques too. Whether «file exists» is a precondition or not is up to the design — I’ve seen plenty of both (although «filename is a valid name for this OS» or «filename follows the naming pattern» makes a sensible candidate for std::logic_error, if it needs to be handled differently from «can’t open»)

Regarding the original question, I’d say exceptions from check_parameters and assert/UB from do_it, unless wrong parameters can be presented to this function within the logically valid workflow (in which case the function should return whatever that logic demands)

Regarding the original question, I’d say exceptions from check_parameters and assert/UB from do_it, unless wrong parameters can be presented to this function within the logically valid workflow (in which case the function should return whatever that logic demands)

Are you assuming that check_parameters() would, for example, check for the presence of a file, or that do_it() would?

I think I’m going to go with option #1, since it makes for the simplest code structure. I can simply throw in the deepest helper functions and maintain all error information, regardless of the semantic nature of the error.

helios wrote:
«How would you implement error handling for this function?«

That depends on what you do with the function. I would only throw an exception if certain circumstances arise that differ from normal program flow. In addition, exceptions should be used on seldom occasions due to its impact on performance and invisable exit points. Finally, throwing too many exceptions may make it harder for you to differentiate between a normal program flow error and a true exceptional circumstance.

Wazzak

@ helios: Is this a real world problem or just a thought experiment? Because although I’m not a programmer by trade I think the biggest issue here is that you are passing an array into this function for processing so no matter which solution you choose, an error in one object affects the processing of every other item in the array. Isn’t that a design flaw? Why not make this function into a member of the class so that each instance of ‘param’ can handle individual issues as they come up? That way data validation is done in the constructor and errors in execution can be handled on a case by case basis. Depending on specifics this approach might also open the door for each object to be processed in parallel.

OpenGL does something good on this I think… it sets an error code for everything which I do not like (or rather, is kinda useless in realistic situations since you’d either have to wrap every GL function to check for an error or have to be very verbose by adding it to every block of code). They give you functions where you supply a callback for logging. When something goes wrong or something could be improved, an implementation defined message will be sent to the callback. It can be heavy though since OGL is used in real-time environments… but maybe it can be better applicated in non-real-time applications?

Last edited on

Computergeek01:

Is this a real world problem or just a thought experiment?

It’s a real problem.

the biggest issue here is that you are passing an array into this function for processing so no matter which solution you choose, an error in one object affects the processing of every other item in the array.

Well, yeah. You normally don’t want your function to run if one of its parameters is undefined due to run time errors.

Why not make this function into a member of the class so that each instance of ‘param’ can handle individual issues as they come up?

Which function? implementation_function()? That doesn’t make a lot of sense. How would a parameter implement the function it’s passed to? And about the other parameters? Does the function get called once per parameter?

That way data validation is done in the constructor and errors in execution can be handled on a case by case basis.

Ah, I see.
Parameters are constructed before the function that will use them is decided. Syntactical checks are of course performed immediately, but that’s about it. Ultimately, the function is the only one who knows if something needs to be a string, or a variable, or whatever.
Now, technically it doesn’t need to be like this. A table stating the number, types, and writability of parameters for each function could be assembled. But some functions are a bitch, really. Like the function that takes any even number of parameters. So eff that.

Depending on specifics this approach might also open the door for each object to be processed in parallel.

That would just be silly. Even with thread pools, the latency incurred is far too great. Checking a parameter costs one or two integer comparisons.

Framework: Since this function is part of a programming language (more specifically, part of an engine), no errors should occur during normal operation, only during script debugging. Assets shouldn’t suddenly disappear, and a script writer shouldn’t intentionally send wrong arguments to a function. So, an error would be a sign that something has gone very wrong — such as data corruption in a user’s machine — or of logical or syntactical errors in the script. Theoretically, the number of errors as a function over time is bounded.

Topic archived. No new replies allowed.

  1. Introduction
  2. Arguments for using Exceptions
    1. Exceptions separate error-handling code from the normal program flow and thus make the code more readable, robust, and extensible.
    2. Throwing an exception is the only clean way to report an error from a constructor.
    3. Exceptions are hard to ignore, unlike error codes.
    4. Exceptions are easily propagated from deeply nested functions.
    5. Exceptions can be, and often are, user defined types that carry much more information than an error code.
    6. Exception objects are matched to the handlers by using the type system.
  3. Arguments against using Exceptions
    1. Exceptions break code structure by creating multiple invisible exit points that make code hard to read and inspect.
    2. Exceptions easily lead to resource leaks, especially in a language that has no built-in garbage collector and finally blocks.
    3. Learning to write Exception safe code is hard.
    4. Exceptions are expensive and break the promise to pay only for what we use.
    5. Exceptions are hard to introduce to legacy code.
    6. Exceptions are easily abused for performing tasks that belong to normal program flow.
  4. Conclusion

1. Introduction

Exceptions have been a part of C++ since early 1990s and are sanctioned by the standard to be the mechanism for writing fault-tolerant code in this language. However, many developers for various reasons choose not to use exceptions, and voices that are skeptical of this language feature are still numerous and loud: Raymond Chen’s article Cleaner, more elegant, and wrong, Joel Spolsky’s blog Exceptions, and Google C++ Style Guide are some of the frequently quoted texts that advise against the use of exceptions.

Rather than taking a side in this debate, I am trying to present a balanced view of pros and cons of using exceptions. The purpose of this article is not to convince readers to use exceptions or error codes, but help them make an informed decision that is best for their particular project. I have structured the article as a list of six arguments for using exceptions and six arguments against it that can often be heard in the C++ community.

2. Arguments for using Exceptions

2.1 Exceptions separate error-handling code from the normal program flow and thus make the code more readable, robust, and extensible.

To illustrate the point, we’ll compare the usage of two simple socket libraries that differ only in the error handling mechanism. Here is how we could use them to fetch HTML from a web site:

string get_html(const char* url, int port)
{
    Socket client(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    client.connect(url, port);
    
    stringstream request_stream;
    request_stream << "GET / HTTP/1.1rnHost: " 
       << url << "rnConnection: Closernrn";

    client.send(request_stream.str());

    return client.receive();
}

Now, consider a version that uses error codes:

Socket::Err_code get_html(const char* url, int port, string* result)
{
    Socket client;
    Socket::Err_code err = client.init(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (err) return err;
        
    err = client.connect(url, port);
    if (err) return err;
    
    stringstream request_stream;
    request_stream << "GET / HTTP/1.1rnHost: " << url 
       << "rnConnection: Closernrn";

    err = client.send(request_stream.str());
    if (err) return err;

    return client.receive(result);
}

In both cases, the code does the same thing, error handling is delegated to the caller, resource cleanup is performed by destructors. The only difference is that in the former case, the socket library throws exceptions in case of failure, and in the latter case, error codes are used.

It is easy to notice that the sample with exceptions has cleaner and simpler flow, without a single if branch to disrupt it. Error handling is completely hidden and only the «normal» code flow is visible. The infrastructure for propagating exceptions is generated by the compiler: in case of an exception, the stack will «unwind» correctly, meaning that local variables in all stack frames will be destroyed properly — including running the destructors.

In fact, sample 2 as shown here is probably even simpler and cleaner than it would be in most cases: we are using only one library within the function, and return that library’s error code. In practice, we would probably have to take into account different error code types that we would hit within the function, and then we would need to map all these error codes to the type we are returning from the function.

What about robustness? When exceptions are used, it is the compiler that produces the code for the «error path» and the programmer does not do it manually, meaning there are less opportunities for introducing bugs. That is especially important when the code is changed — it is very easy to forget updating the error-handling part when a change is introduced to the normal code path, or introduce a bug when doing so.

2.2 Throwing an exception is the only clean way to report an error from a constructor.

The purpose of a constructor is to establish the class invariant. To do that, it often needs to acquire system resources or in general perform an operation that may fail. If a constructor fails to establish the invariant, the object is in invalid state and the caller must be notified. Clearly, error codes cannot be used for that purpose because a constructor is not allowed to return a value. However, an exception can be thrown from a constructor in case of failure, and by doing this, we prevent creation of an invalid object.

What are some alternatives to throwing an exception from a failing constructor? One of the most popular is the «two-stage construction» idiom that is used in our sample 2. The process of constructing an object is split into two steps: the constructor performs only the part of initialization that cannot fail (i.e., setting primitive type members), and the part that can fail goes into a separate function that usually has a name such as init() or create() and returns an error code. Establishing the class invariant in this case involves not only constructing the object, but also calling this other function and checking the returned error code. The downsides of this approach are pretty obvious: it takes more work to correctly initialize an object and it is easy to end up with an object in invalid state without knowing it. Furthermore, copy construction cannot be implemented this way — there is no way to tell the compiler to insert the second step and check for the error code. Having said that, this idiom is pretty effectively used in many libraries, and with some discipline, it may be successful.

Another alternative to throwing an exception from a constructor is maintaining a «bad state flag» as a member variable, setting that flag in the constructor, and exposing a function to check the flag. The standard IO streams use this approach:

ifstream fs("somefile.txt");
if (fs.bad())
    return err_could_not_open;

This technique is similar to the two-stage construction idiom. It requires an additional data member — the state flag which can be a prohibitive expense in some scenarios. On the other hand, copy construction can be implemented this way, although it is far from being safe — for instance, standard containers do a lot of copying internally and there is no way of making them check for the state flag. However, just as with the two-stage construction, this approach can work as long as we know what we are doing.

Other alternatives include setting some global value such as errno in the constructor and hoping that the caller would remember to check it. This approach is clearly inferior to the previous ones and we won’t discuss it here further.

2.3 Exceptions are hard to ignore, unlike error codes.

To illustrate this argument, all we have to do is remove error checking from sample 2 — it will compile just fine and as long as there are no run time errors, it will work fine as well. However, imagine there was an error in the call to init(): the object client will be in invalid state and when its member functions are invoked, pretty much anything can happen depending on the internal implementation of the Socket class, Operating System, etc.; the program may crash immediately or it can even execute all the functions but do nothing and return without any sign that something went wrong — except for the result. On the other hand, if an exception was thrown from the constructor, the invalid object would have never even been constructed and the execution would continue at the exception handler. The usual phrase is: «we would get an exception right in our face».

But is it really that hard to ignore an exception? Let’s go up the stack and see the caller of get_html():

try {
    string html = get_html(url);
}
catch (...)
{}

This horrible piece of code is known as «swallowing exceptions» and the effect is not much different than ignoring error codes. It does take more work to swallow exceptions than to ignore error codes and these constructs are easier to detect during code reviews, but the fact is that exceptions are still pretty easy to ignore, and people do it.

However, even if they are easy to ignore, exceptions are easier to detect than error codes. On many platforms, it is possible to break when an exception is thrown if the process is run from a debugger. For instance, GNU gdb supports «catchpoints» for that purpose, and Windows debuggers support the option «break if an exception is thrown». It is much harder, if not impossible, to get a similar functionality with error codes.

2.4 Exceptions are easily propagated from deeply nested functions.

We are often not able to deal with an error at the point it is originally detected. We need to propagate the information about the error to the level where we can handle it, and exceptions enable jumping directly to the handler without writing all the plumbing manually.

Back at our sample 1 with the Socket class. Assume get_html() is invoked by a function get_title() that gets called by a UI event handler. Something like:

string get_title(const string& url)
{
    string markup = get_html(url);

    HtmlParser parser(markup);
    
    return parser.get_title();
}

Exceptions would probably be handled at the UI event handler level:

void AppDialog::on_button()
{
    try {
        string url = url_text_control.get_text();
        result_pane.set_text(
            get_title(url);
    }
    catch(Socket::Exception& sock_exc) {
        display_network_error_message();
    }
    catch(Parser::Exception& pars_exc) {
        display_parser_errorMessage();
    }
    catch(...) {
        display_unknown_error_message();
    }
}

In the sample above, get_title() does not contain any code for propagating the error information from get_html() to on_button(). If we used error codes instead of exceptions, get_title() would need to check for the return value after calling get_html(), map that value to its own error code, and pass the new error code back to on_button(). In other words, using exceptions would make get_title() look something like our sample 1, and error codes would turn it into something similar to sample 2. Something like:

enum Get_Title_Err {Get_Title_Err_OK, 
                    Get_Title_Err_NetworkError, Get_Title_Err_ParserError};
Get_Title_Err get_title(const string& url, string* title)
{
    string markup;
    Socket::Err_code sock_err = get_html(url.c_str(), &markup);
    if (sock_err) return Get_Title_Err_NetworkError;

    HtmlParser parser;
    HtmlParser::Err_code parser_err = parser.init(markup);
    if (parser_err) return Get_Title_Err_ParserError;
    
    return parser.get_title();
}

Just as with sample 2, sample 7 can easily get more complicated if we try propagating more specific error codes. In that case, our if branches would need to map error codes from the libraries to appropriate Get_Title_Err values. Also, note that we show only a part of the function nesting in this sample — it is not hard to imagine the work needed to propagate an error code from deep within the parser code to our get_title function.

2.5 Exceptions can be, and often are, user defined types that carry much more information than an error code.

An error code is usually an integer type and can carry only so much information about the error. Microsoft’s HRESULT type is actually a pretty impressive attempt to pack as much information in a 32-bit integer as possible, but it clearly shows the limits of that approach. Of course, it is possible to have an error code that is a full-blown object, but the cost of copying such an object multiple times until it reaches the error handler makes this technique not desirable.

On the other hand, exceptions usually are objects and can carry a lot of useful information about the error. It is a pretty common practice for an exception to carry information about the source file and line that it was thrown at (using macros such as _FILE_ and _LINE_) and they can even automatically send messages to a logging system.

Why is it expensive to use objects for return error codes and not for exceptions? There are two reasons: first, an exception object is created only if an error actually happens, and that should be an exceptional event — pun intended. The error code needs to be created even if the operation succeeds. Another reason is that an exception is usually propagated by reference to the handler and there is no need to copy the exception object.

2.6 Exception objects are matched to the handlers by using the type system.

Let’s expand a little on our sample 6 and display a specific error message in case the error happened when establishing the socket connection:

void AppDialog::on_button()
{
    try {
        string url = url_text_control.get_text();
        result_pane.set_text(
            get_title(url));
    }
    catch(Socket::SocketConnectionException& sock_conn_exc) {
        display_network_connection_error_message();
    }
    catch(Socket::Exception& sock_exc) {
        display_general_network_error_message();
    }
    catch(Parser::Exception& pars_exc) {
        display_parser_errorMessage();
    }
    catch(...) {
        display_unknown_error_message();
    }
}

Sample 8 demonstrates the use of the type system to classify exceptions. The handlers go from most specific ones to the most general one, and this is expressed with the language mechanism that serves the purpose: inheritance. In our sample, Socket::SocketConnectionException is derived from Socket::Exception.

If we were using error codes instead of exceptions, the error handler would probably be a switch statement and each individual case would handle a value for the error code. default would most probably correspond to catch(...). It is doable, but not quite the same. In sample 8, we use a handler for Socket::Exception to handle all exceptions from the socket library except SocketConnectionException that we handle just above it; with error codes, we would need to list all these other error codes explicitly. If we forget one, or maybe add a new error code later but forget to update it… you get the picture.

3. Arguments against using Exceptions

3.1 Exceptions break code structure by creating multiple invisible exit points that make code hard to read and inspect.

That sounds exactly the opposite of what we talked about in 2.1. How is it possible that exceptions simultaneously make the code hard to read and easy to read? To understand this point of view, remember that by using exceptions we managed to display only the «normal flow». The error-handling code still gets generated by the compiler for us and it has its own flow which is orthogonal to the flow of the code we wrote. In effect, the normal code flow is cleaner and easier to read and understand with exceptions, but that is only a part of the story. Now we need to be aware that at any function call, we introduce an invisible branch that, in case of an exception, immediately exists (or jumps to the handler if it is in the same function). That sounds worse than goto and comparable to longjmp, but in fairness, C++ exceptions are much safer to use than longjmp: as we saw in section 2.1, the compiler generates the code that unwinds the stack.

Still, the problem with readability is not necessarily bogus: if we are taking a quick look at a piece of code to learn what it does, the normal flow is really all we need, but what if we need to get really knowledgeable about it? For instance, imagine you need to do a code review or extend a function — would you rather have all possible code paths right in front of you, or having to guess which function can throw an exception and under what circumstances? Granted, it is much less of a concern if you make sure that exceptions are used only for error handling, and that the code was originally written with exception-safety in mind.

3.2 Exceptions easily lead to resource leaks, especially in a language that has no built-in garbage collector and finally blocks.

To understand what we are talking about here, let’s for a moment forget object oriented programming with its classes, destructors, and exceptions, and fall back to good old C. Many functions take the following form:

void func()
{
    acquire_resource_1();
    acquire_resource_2();

    use_resources_1();
    use_resources_2();
    use_resources_3();

    release_resource_2();
    release_resource_1();
}

Basically, we acquire some resources at the beginning of the function, use them to do some processing, and then release them. The resources in question can come from the system (memory, file handles, sockets…), or external libraries. In any case, they are limited and need to be released, or may be exhausted at some point and that’s what we call a «leak».

So where is the problem? Our function from sample 9 is an example of well-structured code with a single entry and a single exit, and we will always clean up the resources at the end of the function. Imagine now that something can go wrong at use_resources_1(). In that case, we don’t want to execute the rest of the use_resources_... functions, but report an error and exit immediately. Well, not immediately — we need to clean up the resources first. How do we do it best with a language like C? Let’s just say the discussions on that topic involve even more passion than our little «exceptions vs. error codes» dilemma: some people copy and paste the clean-up code and call it whenever they exit; other ones make macros for that purpose when possible; there are developers who preserve the «SESE» structure of the function and introduce if-else branches for each function that can fail. Such functions look like a closing angle bracket «>» and some call it «arrow anti-pattern». Many C developers use goto to jump to the cleanup part of the function.

One way or another, it is a mess even without exceptions. What if we switch to C++ and use_resources_1() to throw an exception in case of error? The way the function is written, the cleanup part will not be executed and we will have a leak. How would a garbage collector and finally blocks help here? Pretend for a moment that Java does not have networking support in the standard library and that we use something similar to our sample 1 but from Java:


public String getHtml(String url, int port) throws (Socket.Exception)
{
    Socket client = null;
    try {
        client = new Socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        client.connect(url, port);

        StringBuffer requestStream = new StringBuffer("GET / HTTP/1.1rnHost: ");
        requestStream.append(url).append("rnConnection: Closernrn");

        client.send(requestStream.toString());

        return client.receive();
    }
    finally {
        if (client != null)
            client.close();
    }
}

In the getHtml method above, we have acquired some resources: a socket, a string buffer, and a couple of temporary string objects. The garbage collector takes care of the objects that ultimately need to release only memory to the system, and the socket is closed in the finally block which is executed when we leave the try block — whether we do it «normally» or via an exception.

C++ does not have a built-in garbage collector, nor does it support finally blocks; how come our function from sample 1 (or sample 2 for that matter) does not leak? The answer is usually called RAII (resource acquisition is initialization), and it really means that objects declared as local variables get destroyed after they go out of scope, regardless of the way they exit that scope. When they get destroyed, the destructors are executed first, and then the memory is returned to the system. It is the burden of the destructors to release all resources acquired by the objects — in fact the destructor of the Socket class used in sample 1 probably looks very much like the body of the finally block in sample 10. The best part is that in C++, we need to write this clean-up code only once and it is executed automatically each time an object goes out of scope. Another nice aspect of RAII is that it treats all resources in a uniform way — there is no need to distinguish between «GC-collectible» resources and «unmanaged» ones and leave the former to GC and clean up the latter in finally blocks: the cleanup is invisible, yet it reliably happens, much like error reporting with exceptions.

Am I saying here that the lack of a garbage collector and finally blocks is not really a reason not to use exceptions in C++ because RAII is a superior method of managing resources? Yes and no. RAII is indeed superior to the combination of a garbage collector and finally blocks, and it is a very straightforward idiom and easy to use. Yet, in order to get the benefits of RAII, it needs to be used consistently. There is a surprising amount of C++ code out there that looks like:

string get_html(const char* url, int port)
{
    Socket client = new Socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    client->connect(url, port);
    
    stringstream request_stream;
    request_stream << "GET / HTTP/1.1rnHost: " << url 
      << "rnConnection: Closernrn";

    client->send(request_stream.str());

    string html = client->receive();

    delete client;
    return html;
}

In sample 11, RAII is used for all resources except the socket — yet it is a disaster waiting to happen: if an exception is thrown anywhere in the function, we leak a socket. If someone unplugs a network cable, our program may crash very quickly. Sure, it is possible to wrap the function in a trycatch block and then close the socket both in the catch block and after it — thus simulating the non-existing finally construct, but it is exactly the kind of repetitive and tedious task that sooner or later is forgotten and we have a serious bug.

But how common this kind of code is anyway? Creating objects on stack, when possible, is not only safer but also easier than pairing new and delete — one would expect that most programmers use RAII all the time. Unfortunately, this is not really the case. There is a lot of code out there that looks like sample 11. For instance, find the official sample code for the SAX2 parser of the popular Xerces-C library — it creates objects on the heap and deletes them after they are used — in case of an exception, there is a leak. In the early 1990s, it was considered «more object oriented» to create an object on the heap even if there were no reasons to do so. Now, many young C++ programmers learn Java first at universities and write C++ code in a Java-like manner rather than idiomatic C++. One way or another, there is a lot of existing code that does not rely on deterministic destructors to automatically release resources. If you are dealing with a code base like that, the only sane thing to do is use error codes and turn the exceptions off.

Note that preventing resource leaks is only a part of the exception safety puzzle. Sometimes a function needs to be transactional: it needs either to succeed or leave the state unchanged. In this scenario, we need to roll back the operation in case of failure before leaving the function. Unsurprisingly, we again turn to destructors to trigger such an operation if an exception is thrown. This idiom is called «Scope Guard» and rather than discussing it here, I suggest reading the original article by Andrei Alexandrescu and Petru Marginean.

3.3 Learning to write Exception safe code is hard.

The usual counter argument to this statement is that error handling in general is hard, and exceptions make it easier, not harder. While there is a lot of truth in that, the issue of the learning curve remains: complex language features often do help experienced programmers simplify their code, but the beginners mostly see the added pain of learning. To use exceptions in production code, a C++ programmer not only has to learn the language mechanism, but also the best practices of programming with exceptions. How soon a beginner learns to never throw from a destructor? Or use exceptions for exceptional conditions only and never for the normal code flow? Or catch by reference, and not by value or a pointer? How about usefulness (or the lack or thereof) of exception specifications? Exception safety guarantees? Dark corners, such as polymorphic throwing?

From a «glass half full» point of view, with exceptions or without them C++ has always been an expert-friendly language that offers very little hand-holding to beginners. Rather than relying on the constraints of the language, a good C++ programmer turns to the community to teach him the best practices of using the language. The guidelines from the community change faster than the language itself: someone who switched from C++ to Java in 1997 often gets very confused when presented with modern C++ code — it looks different, yet the language itself has not changed much. Back to the exceptions, the C++ community learned a lot on how to effectively use them since they were first introduced to the language; someone who follows new publications and newsgroups should pick up the best practices with some effort. A good coding standard that follows the community’s best practices helps a lot with the learning curve.

3.4 Exceptions are expensive and break the promise to pay only for what we use.

When talking about the performance impact of a language feature or a library, usually the best advice to a developer is to measure and decide for himself or herself whether there is any performance impact and if it is acceptable in their scenarios. Unfortunately, sometimes it gets pretty hard to follow this advice when it comes to exceptions, mostly because the performance of an exception handling mechanism is very implementation dependent; therefore, the conclusions made based on measurements with one compiler may be invalid if the code is ported to another compiler, or even another version of the same compiler. At least, the good news is that compilers are getting better in producing efficient code in the presence of exceptions and it is likely that if exceptions are not impacting the performance of your code now, they will do it even less in the future.

To understand the performance impact of exception handling in general, I strongly advise reading Chapter 5.4 of Technical Report on C++ Performance (draft). It discusses in some detail the sources of performance overhead: the code added to trycatch blocks by the compiler, impact on the regular functions, and the cost of actually throwing an exception. It also compares the two most common implementation approaches: the «code» approach where the code is added to each trycatch block, and the «table» approach where the compiler generates static tables.

After getting familiar with the performance impact scenarios of exceptions in general, the best thing to do is research the topic for your particular platform and compiler. A very nice and relatively recent presentation given by Microsoft’s Kevin Frei can be found at the Northwest C++ Users Group and it covers the Visual C++ compiler for 32 and 64 bit Windows platform. Another very interesting text related to the GNU C++ compiler on Itanium is Itanium C++ ABI: Exception Handling.

Before concluding the discussion on performance and exceptions, it would be worthwhile to add that current C++ implementations do not offer predictability guarantees for exceptions, which make them unsuitable for hard real-time systems.

3.5 Exceptions are hard to introduce to legacy code.

In other words, exception safety must never be an afterthought. If the code is written without exceptions in mind, it is much better to keep exceptions out of it or completely rewrite it — there are just too many ways things can go wrong. In this aspect, it reminds me of making a code thread safe if it was originally written without thread safety in mind — just don’t do it! Not only is it hard to review the code for potential problems, but more importantly, it is hard to test. How are you going to trigger all the possible scenarios that lead to exceptions being thrown?

On the other hand, if the code base is modular, it is quite possible to write new code with exceptions that would peacefully coexist with the old code. The important thing is to draw the boundaries and not let exceptions cross them.

3.6 Exceptions are easily abused for performing tasks that belong to normal program flow.

Nice aspects of using exceptions, especially the fact that they can be easily propagated from deeply nested code structures, make them tempting to be used for tasks other than error handling — most likely for some kind of poor man’s continuations.

Almost always, using exceptions to affect the «normal» flow is a bad idea. As we already discussed in section 3.1, exceptions generate invisible code paths. These code paths are arguably acceptable if they get executed only in the error handling scenarios. However, if we use exceptions for any other purpose, our «normal» code execution is divided into a visible and invisible part and it makes code very hard to read, understand, and extend.

Even if we accept that exceptions are meant to be used for error handling only, sometimes it is not obvious if a code branch represents an error handling scenario or normal flow. For instance, if std::map::find() results in a value not found, is it an error or normal flow? Sometimes, an answer is easier to find if we think in terms of «exceptional», rather than «error» state. In our example, is it an exceptional case that a value is not found in a map? In general, the answer is probably «no» — there are many «normal» scenarios in which we are checking whether a value is stored in a map and branch based on the result — none of these branches is an error state. That’s why map::find does not throw in case there is no requested key in the map.

How about a functionality that depends on users’ input? If a user enters invalid data, is it better to throw or not? Users’ input is better considered invalid unless proven otherwise and, if possible, there should be some function that validates the input before it is processed. For these validation functions, invalid data is an expected outcome, and there is no reason for it to trigger an exception. The problem is, it is often impossible to validate the input before actually processing it (parsers, for instance); in such cases, using exceptions to break the processing and report an error is often a good approach. HtmlParser from our sample 5 throws if it cannot parse broken HTML.

My personal opinion is that an exception should never be thrown if a program is working in normal conditions and with valid data. In other words, when you test a program by running it from a debugger, be sure to set a «catchpoint» that triggers whenever an exception is thrown. If the program execution breaks because an exception is thrown, that signals either a bug that should be taken care of, or a misuse of an exception handling mechanism and that should be fixed as well. Of course, an exception may be triggered due to abnormal conditions of the environment (for instance, network connection suddenly gets lost, or a font handle allocation fails) or invalid input data and in that case the exception is thrown for a good reason.

4. Conclusion

There is no simple answer to the «exceptions or error codes» question. The decision needs to be made based on a specific situation that a development team faces. Some rough guidelines may be:

  • If you have a good development process and code standards that are actually being followed, if you are writing modern-style C++ code that relies on RAII to clean up resources for you, if your code base is modular, using exceptions may be a good idea.
  • If you are working with code that was not written with exception safety in mind, if you feel there is a lack of discipline in your development team, or if you are developing hard real-time systems, you should probably not use exceptions.

Exceptions in Programming

Published 2011-02-26 – Updated 2011-02-26

Introduction

On the use and value of exceptions in programming — a discussion of how and when to use exceptions and whether or not the exceptions thrown by a method should be declared and their handling enforced by the compiler.

Note: Specific examples in this article are drawn from the Java programming language, since that’s where I have extensive experience with exceptions.

Error Codes vs. Exceptions back to top

In the Beginning, Man Created the Status Code

The earliest programming languages, such as BASIC, COBOL and RPG, had no concept of a function call. If a subroutine needed to indicate that an error had occurred, the programmer needed to set aside a global variable in which to store the subroutine result. Since the result code was usually not unique to any specific subroutine each subroutine had to be careful to explicitly set it prior to completion, to either an error indication or some agreed upon “no error” value, typically zero.

The C language, which rose to mainstream popularity in the late 1970’s and still remains immensely popular today, had functions with arguments and a return value. In many APIs, such as those for system I/O, the functions returned a result code which was positive if the function was successful (including 0), or -1 if there was an error. In the event -1 was returned, a global status variable, errno, indicated the specific condition. Other APIs, such as those memory management, returned 0 or null on failure and non-zero on success.

Was That a Stop Sign Back There?

The essential problem with passive error signaling such as return codes and global status codes is that they require the caller of the API to actually check the result after each and every API call. A common source of very difficult to debug errors when programming in C, even today, is allocating memory and failing to verify that the pointer returned is not null. In this case the program blunders on and overwrites memory in the data segment starting at offset zero, corrupting the program’s memory space, or in older operating systems, the system’s memory space, and often not triggering a real fault until some time later — sometimes not until much, much later.

To use an analogy, the programmer would write a program which blissfully blows through stop signs. Most of the time there is no cross-traffic (no failure) and therefore no problem… but at some point there is the inevitable collision with a semi-truck coming through the intersection in another direction.

A big problem with unchecked result codes is that most of the time the program works as designed; it’s not until something fails that things come undone, and then they tend to really hit the fan in some spectacular manner… but often not until some point far removed from the actual cause of the failure, and always without any information of what actually when wrong.

Another problem is that all those if(ret!=0) { ... } statements after every function call which clutter up the code and get in the way of the program flow and the programmer’s thinking as the program is created.

Enter Stage Left… Exceptions

Exceptions are a mechanism in a programming language that signal an error by interrupting the program flow and unrolling the call stack until the exception is handled. In the vernacular, an exception is thrown by the code which triggers it, and is caught by the code which handles it. In conjunction with constructs such as try/catch/finally and using/dispose the code between throw point and the catch point can clean up such that the program state for that code remains valid.

Exceptions allow the programmer to use an API’s methods with the core assumption that they will succeed, which allows the logical flow of the code to remain uncluttered with error handling. Instead, the error handling is consolidated at the key points at which the programmer can take action. More importantly, the program execution does not blunder on, blissfully unaware that it just failed. Essentially, exceptions allow the program to fail-fast with no effort required on the part of the caller — while the program has still failed, it has done so in a clear and identifiable manner.

Exceptions in modern languages which record the call stack provide another crucial piece in the puzzle, that is, the exact location at which the failure occurred.

Exceptions have been conceptualized in two essential forms, checked and unchecked.

Checked Exceptions back to top

To date, the only language which supports checked exceptions is Java, in which it is commonly held that this was done as an academic experiment. Many in the industry consider checked exceptions to be a failed experiment. For myself, the jury is still out, although all my programming in Java has used unchecked exceptions.

A checked exception is one which must be handled in the method in which it is thrown or be declared as being thrown by that method. Furthermore, any method which uses another method which declares that it throws a checked exception must itself either handle the exception or declare it to be thrown. In this way the compiler, assuming rigorous and correct use of checked exceptions, can at least in theory enforce that all exceptional conditions are handled by the code.

Underlying this is the assumption that any condition which the program should at all be able to handle is thrown as a checked exception. The benefit is presumed to be that when the program is complete the programmer will have given adequate consideration to the ways in which it can fail and have already written the proper error handling so that it either recovers to the maximum extent possible, or it fails “gracefully”.

The idea and hope is that problems are then found during development and do not cause a program failure at runtime. This is in keeping with the generally accepted wisdom that it’s better to find as many problems as possible at compilation time.

The particular and primary benefit to using checked exceptions is that they are forcibly and rigorously documented. It’s not possible for the programmer to forget to document the ways in which the code can fail, because the compiler and the documentation system are explicitly informed of said ways. And it’s not possible for the programmer to mask exceptions which arise from called code because, again, the compiler requires that they become part of the contract of the calling method if they are not handled.

Critical to making checked exceptions work is to handle and transform exceptions which they cross API boundaries. This means that if some code uses another API package, any exceptions thrown by that other package must be caught and transformed into an exception belonging to the calling code. That is, if package X uses package Y it’s important that package X never throw a YException — instead it must transform a YException into an XException and throw that.

Checked exceptions have, however, exhibited several problems in practice which has undermined there apparent utility. In particular, they have been accused of working well in small examples but failing to scale well to larger programs (I must confess that I am not yet convinced yet that this is the case, although I do lean that way).

Considering Errors Too Soon

The primary complaint with checked exceptions is that they force a programmer to think about problems too soon. That is, when the programmer should be concerned with laying down the program logic, they are instead forced to think about what can go wrong and what to do about it.

This has led to a common mistake of swallowing exceptions, a problem made worse by many IDE’s which shell in a template such as:

InputStream                         inp=null;

try {
    inp=new FileInputStream("SomeFile.txt");
    (more code here)
    inp.close();
    }
catch(IOException thr) {
    // ignore exception for now
    }

Here is the worst of all possible ways to handle an exception — swallowing it. In this case the programmer has “temporarily” deferred the error-handling consideration with a stub block that does nothing. Except that it does something very subtle and very important — it silently ignores the exception. In doing so it re-introduces the blundering on problem which existed with error codes. Unless the programmer is diligent to return to the code and fix the error handling, it’s just silently broken.

For a long time, I used to handle Java’s InterruptedException this way. As a result I had a lot of code which did not correctly handle being interrupted, usually because it simply silently ignored the fact that it was interrupted.

Now the argument can surely be made, and might even be valid, that the code is not complete until the error handling has been done, and I’m not going to disagree with that. But the fact of the matter is that many programmers will simply push the error handling to one side while they “first get it working”. When I was first learning to program in Java I was guilty of this on multiple occasions.

Like it or not, one of the side-effects of forced error handling is harmful error handling, which is worse than no error handling.

Considering Errors Out of Context

Another related problem is lacking adequate context about how and why the exception occurs and how to best handle it when it does. The result of this is typically inadequate error handling.

It takes considerable effort to mentally step sideways from the task at hand, which is writing the code to accomplish the current goal, and think about what things can go wrong and what can be done when they do. This task much easier in the light of a concrete example of when it has gone wrong; however, that context often arrives very late in the process — possibly after shipping when the customer is trying to use the software.

Assumption of Guilt: Phantom Exceptions

The essential problem here is that many problems are technically possible, but in practice only theoretical. This occurs especially when preconditions have already been checked by the caller. This is the “guilty until proven innocent” effect. I call these phantom exceptions.

The end-result tends to be extraneous error handling which throws an unchecked exception with the comment that it can never happen. And that’s if the programmer is handling it well; the big temptation is to just swallow the exception, which is fine if it truly never happens… until it does under some weird circumstance.

Good examples are FileNotFoundException when the file is checked for existence first and UnsupportedEncodingException when the encoding is mandatory:

public void readConfigFiles() {
    parseFile(new File("Base.json"));

    File custom=new File("Custom.json");
    if(custom.exists()) { parseFile(custom); }
    }

private void parseFile(File src) {
    InputReader                     inp=null;

    try {
        inp=new BufferedReader(new InputStreamReader(new FileInputStream(src),"UTF-8"),1024);
        (input parsing here)
        inp.close();
        }
    catch(FileNotFoundException thr) {
        // can't happen - file must exist
        }
    catch(UnsupportedEncodingException thr) {
        // can't happen - UTF-8 is required by JVM spec.
        }
    catch(IOException thr) {
        (code here to show the user a window with the exception details)
        System.exit(0); // can't continue without config!
        }
    finally {
        // ignore IO problems with close and NullPointerException if open failed to avoid masking real exception
        try { inp.close(); } catch(Throwable thr) {;}
        }
    }

The problem here is that in all likelihood the programmer is quite correct, the two exceptions which are simply ignored will probably never happen, but they were forced to construct a lot of boiler plate code to “handle” them. And the handling is incorrect in the event that they actually do (either because the programmer was mistaken, or because something truly unusual happened, like a disk failure or an administrator deleting the directory as the program is started).

Another common example is with factory APIs such as Java’s security support:

try { MessageDigest md = MessageDigest.getInstance("MD5"); }
catch (NoSuchAlgorithmException thr) { throw new RuntimeException(thr.toString(),thr); }

As well, there’s the good ol’ URL exception with a constant (or other known good) URL:

try { URL u = new URL("http://www.yahoo.com/"); }
catch (MalformedURLException thr) { throw new RuntimeException(thr.toString(),thr); }

I have learned to never ever silently discard an exception like this. Nor do I log the stack-trace and carry on. Since the introduction of exception chaining in Java 4, the best way to handle these is as follows:

    try {
        inp=new BufferedReader(new InputStreamReader(new FileInputStream(src),"UTF-8"),1024);
        (input parsing here)
        inp.close();
        }
    catch(FileNotFoundException thr) {
        throw new RuntimeException(thr.toString(),thr); // can't happen - file must exist
        }
    catch(UnsupportedEncodingException thr) {
        throw new RuntimeException(thr.toString(),thr); // can't happen - UTF-8 is required by JVM spec.
        }
    catch(IOException thr) {
        (code here to show the user a window with the exception details)
        System.exit(0); // can't continue without config!
        }
    finally {
        // ignore IO problems with close and NullPointerException if open failed to avoid masking real exception
        try { inp.close(); } catch(Throwable thr) {;}
        }

and in Java 7 the boiler-plate can be reduced with multi-exception clauses:

    try {
        inp=new BufferedReader(new InputStreamReader(new FileInputStream(src),"UTF-8"),1024);
        (input parsing here)
        inp.close();
        }
    catch(FileNotFoundException, UnsupportedEncodingException thr) {
        throw new RuntimeException(thr.toString(),thr); // file must exist and UTF-8 is required by JVM spec
        }
    catch(IOException thr) {
        (code here to show the user a window with the exception details)
        System.exit(0); // can't continue without config!
        }
    finally {
        // ignore IO problems with close and NullPointerException if open failed to avoid masking real exception
        try { inp.close(); } catch(Throwable thr) {;}
        }

Lastly, the try/catch/ignore on close() is itself an interesting example. Close on a file input stream can never actually throw an exception, but close() is declared as throwing IOException just in case it matters for some input stream. More importantly, an exception on close will almost certainly be preceded by an exception while reading, and nothing can be done with the close exception in the finally block which would not obscure the real exception that preceded it. In the end, the finally clause is really to make a best effort to release the file handle resource.

Here, the real underlying flaw is that whether an exception should be checked is sometimes context dependent.

Unexceptional Exceptions

These occur usually as the misuse of checked exceptions. They are, in fact, checked exceptions which represent an error on the part of the caller which can be fixed by correcting the call. This would be things like using a checked exception to signal that a passed argument is invalid and other assertion-like checks.

The real flaw highlighted with these kinds of exceptions is the burden placed on the programmer to correctly determine when an exception should be checked and when it should be unchecked.

Throws Clause Proliferation

This occurs when the rule to transform exceptions across API boundaries is violated. The symptom is a method that declares itself as throwing any more than a single exception appropriate to it’s own abstraction layer. It looks like:

public void readConfig(...parms...) throws IOException, SocketException, SQLException, DOMException {

A bigger problem with this situation is that it causes abstraction leakage. Ideally the caller of readConfig show not be concerned with, nor even be aware of, the fact that someMethod uses I/O, sockets, SQL or XML API’s. This information represents leakage to the caller. The caller should be concerned with configuration errors which express an abstraction appropriate to a configuration abstraction. If an attempt is made to read an invalidly formed XML configuration file then that should be returned as, for example, a ConfigurationFormatException, not as a DOMException. Among other things, if the configuration format is JSON then an XML exception is utterly irrelevant, but a format exception remains just as applicable to JSON text as to XML text:

public void readConfig(...parms...) throws ConfigException {

Exception Coarsening

The result of another shortcut, this time an attempt to avoid lengthy throws clauses, this is when a declares that it throws some common superclass instead of specific detailed exceptions. The pathological cases are throws Exception and throws Throwable. Technically the programmer has met the demands of the compiler. However, the specific causes for failure have been lost and all the caller knows is that something can go wrong — well, gee wiz genius, I could have guessed that!

Declaring throws IOException is not much better, although many Java class-library do exactly that. The question is: “Well, what kind of I/O exception might occur”. Without that information, handling the error correctly is often impossible except in the vaguest of terms — often simply present the error text to the user and hoping that they can figure out what to do differently.

More importantly, because this kind of throws declaration does nothing to aid the caller in error handling it often does nothing more than make the programmer have to consider what to do if an I/O exception occurs, which, absent specifics, is only marginally better than using unchecked exceptions.

Interface Declares Exception Which the Implementation Does Not Throw

This occurs when a particular implementation of some pre-defined interface (small i) does not actually need to throw an exception declared by the defining interface. Consider the following class:

public class NullOutputStream
extends OutputStream
{
public NullOutputStream() {
    }

public void write(int ign) {
    }

public void write(byte[] ign) {
    }

public void write(byte[] ign, int ofs, int len) {
    }

public void flush() {
    }

public void close() {
    }
}

This class cannot throw exceptions, by its very definition, however a perfectly reasonable use-case illustrated next won’t compile:

class Test
extends Object
{
public void main(String... args) {
    Writer wtr=new OutputStreamWriter(new NullOutputStream());
    wtr.write(0);
    wtr.close();
    }
}

Why not? Because write() and close() of Writer are declared to throw IOException, and the compiler can’t know that in this case they cannot actually do so. This is a special case of phantom exceptions caused by the exceptions contracted in the interface not being applicable to the implementation.

Interface Does Not Permit an Exception Which the Implementation Needs To Throw

This occurs when a reasonable interface design declares some exception class and the implementation does not neatly (or even remotely) fit within the conceptual bounds of the declared interface. It usually occurs as the result of the interface not declaring an appropriate exception abstraction, often instead inheriting the basis exception from whatever made sense in the initial implementation (the one which demanded the creation of the interface in the first place).

This might happen, for example, when an interface is created in the context of an initial implementation using an SQL database. The interface might subsequently end up with a throws SQLException without thought or consideration given to a future implementation which sources data from text files, or a data-store, or some other database system.

This illustrates a particularly difficult burden placed on an interface designer due to checked exceptions. One has to carefully consider the broader context of the interface abstraction. But the pay-back is that the interface user gets to have a clearly defined base for the exceptions which can occur. However, since that exception base is necessarily inexplicit, the caller has no real idea of exactly what can go wrong absent documentation, which again, is little better that unchecked exceptions.

Middleware

A particular problem occurs when the calling code is separated from the implementation by some sort of generic middleware layer including that which occurs with callback style APIs. The problem is that the middleware cannot generally predict nor effectively limit the scope for exceptions which can occur. This is because it is utterly unaware of the context of the exception except in the broadest of terms.

Consider a simple generic callback interface:

public interface ObjectProcessor<T>
{
public void processObject(T obj);
}

a DirectoryTree class:

class DirectoryTree
extends Object
{
...
public void processTree(ObjectProcessor prc) {
    (walks the tree and invokes prc for each file system entry)
    }
}

and a DataStore class:

public class DataStore
extends Object
{
...
public void saveStore(ObjectProcessor prc) {
    (walks the b-tree index and invokes prc for each record)
    }
}

The ObjectProcessor for the DirectoryTree wants to throw a DirectoryTreeException while the data store ObjectProcessor wants to throw a DataStoreException. The interface’s processObjects method can’t declare itself as throwing anything useful to these two diverse implementations; at best it can declare throws Exception, which once again tells the caller nothing useful.

Unchecked Exceptions back to top

Unchecked exceptions do not require explicit handling nor a throws declaration. Consequently their biggest shortcoming is that they are easily left undocumented and easily bleed through abstraction layers. They require more discipline on the part of the caller to consider the exceptions declared in the package and appropriately transform them.

However, because their handling is not forced it is much less common for them to be mishandled. This is because their handling tends to be in the context of specific need, either because of forethought or because the exception actually occurs during testing.

I have noted, with several thousand classes written, that the theoretical problems with unchecked exceptions seem to have been exactly that, theoretical. In practice the nesting of code layers is quite shallow and many of the exceptions which could crop up in theory never do in practice.

Coded Exceptions — Taming Class Explosion back to top

One final thought pertains to the use of coded exceptions. In this style, instead of using a separate exception class for every error condition, I use one exception per package, and define specific error codes in that exception. This avoids one problem of Java exceptions, being the code bloat caused by the large number of specific error conditions that can accumulate in a large code-base.

An example might be ConfigException:

public class ConfigException
extends Exception
{
private final String                    code;

public ConfigException(String cod, String msg) {
    this(cod,msg,null);
    }

public ConfigException(String cod, String msg, Throwable cau) {
    super(msg,cau);
    code=cod;
    }

public String getMessage() { return (code+": "+super.getMessage()); }

public String getCode() { return code; }

public boolean isCode(String val) { return (code.equalsIgnoreCase(val); }

// *************************************************************************************************
// STATIC PROPERTIES - EXCEPTION CODES
// *************************************************************************************************

static public final String              SOURCE_NOT_FOUND    ="SourceNotFound";
static public final String              FORMAT_INVALID      ="FormatInvalid";
(etc)
}

One of the negative side effects of this style is that specific exception codes can be added at any time without breaking a throws ConfigException contract, which means you should always handle specific conditions with a final default other code clause. However, this is no different than an API adding a new subclass of a parent exception, for example, adding ConfigSourceNotFoundException as a subclass of ConfigException (except that it’s a lot more concise).

With this kind of exception, handling usually takes the form:

public void readConfigFiles() {
    try {
        parseFile(new File("Base.json"));

        File custom=new File("Custom.json");
        if(custom.exists()) { parseFile(custom); }
        }
    catch(ConfigException thr) {
        if(thr.isCode(ConfigException.SOURCE_NOT_FOUND)) {
            (handle missing config file)
            }
        else if(thr.isCode(ConfigException.FORMAT_INVALID)) {
            (handle invalid format)
            }
        else {
            throw new ApplicationException(ApplicationException.CONFIG_ERROR,("Unhandled error reading config: "+thr),thr);
        }
    }

Which ends up being quite clean in practice.

Conclusions back to top

Hopefully, if there is one thing that becomes clear here, is that there are no categorically correct answers. Handling errors is hard, and handling errors well is even harder. Like many things in computer programming, it is as much art as it is science.

My thinking, lately, is leaning towards the idea that checked exceptions might be most useful in library packages and less useful or undesirable in application software.

Further Reading back to top

  • Bruce Eckel: Does Java Need Checked Exceptions
  • Elliotte Rusty Harold: Bruce Eckel is Wrong
  • Cédric Beust: Improving Exceptions
  • Barry Ruzek: Effective Java Exceptions
  • Rod Waldhoff: Checked Exceptions were a Mistake
  • Alan Griggiths: Exceptional Java

Понравилась статья? Поделить с друзьями:
  • Except http error
  • Except error as error nameerror name error is not defined
  • Except connection error python
  • Excel число отформатировано как текст как исправить
  • Excel файл большого размера как исправить