Menu

C# try catch: catch, when, finally로 예외 처리하기

try, catch, finally는 C#이 예외를 처리하는 방법입니다. 예외가 호출 스택을 따라 올라가는 방식, 특정 예외 타입을 올바른 순서로 잡는 방법, when으로 필터링하기, finally에서 정리하기, 스택 추적을 잃지 않고 throw;로 다시 던지기, 그리고 잡지 말아야 할 예외를 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

실행 중에 무언가 잘못되면(파일이 없거나, 텍스트가 숫자가 아니거나, 키가 딕셔너리에 없거나) .NET은 예외, 즉 오류를 설명하는 객체를 던집니다. 예외는 catch 블록이 처리할 때까지 호출 스택을 따라 올라갑니다. 아무도 처리하지 않으면 프로그램이 멈추고 오류를 출력합니다.

기본 try catch

실패할 수 있는 코드를 try로 감싸고, 실패는 catch에서 처리합니다:

출력:

42 doubled is 84
'forty-two' is not a number
7 doubled is 14
Still running

int.Parse("forty-two")가 예외를 던지면 try 블록에서 그 뒤의 Console.WriteLine은 건너뛰고, catch (FormatException) 블록이 실행되며, 루프는 계속됩니다. 예외 객체가 필요 없다면 변수를 생략할 수 있습니다(catch (FormatException)).

이 경우에는 더 나은 도구가 있습니다. int.TryParse(input, out int n)은 예외를 던지는 대신 false를 반환합니다. 예외는 코드가 예상하지 못한 상황을 위한 것입니다. 자주 잘못되는 입력은 예상된 것이므로, 잡지 말고 확인하세요.

예외가 이동하는 방식

호출 체인 깊은 곳에서 던져진 예외는 일치하는 처리기를 찾을 때까지 모든 메서드를 풀어 나갑니다. 각 메서드에서 throw 뒤의 코드는 실행되지 않습니다.

출력:

OrderTotal finished
5.00
Caught KeyNotFoundException in Main

PriceOf와 OrderTotal에는 catch가 없으므로 예외는 그대로 통과해 Main에 도달합니다. 실패에 대해 무엇을 할지 아는 수준에 처리기를 두세요. 그곳은 대개 오류가 일어난 곳이 아닙니다.

특정 예외를 올바른 순서로 잡기

catch 절은 자기 타입과 그로부터 파생된 모든 타입을 처리합니다. 절이 여러 개면 처음 일치하는 것이 이기므로, 가장 구체적인 것부터 가장 일반적인 것 순으로 두세요:

출력:

10 / 2 = 5
Cannot divide by zero
Both values must be whole numbers
Unexpected: OverflowException

마지막 호출은 어느 구체적인 절도 처리하지 않는 OverflowException을 던지므로 일반적인 catch (Exception e)가 처리합니다. catch (Exception)을 먼저 두면 그 뒤의 절들이 도달할 수 없게 되며, 컴파일러는 이를 CS0160 오류로 거부합니다.

when을 쓰는 예외 필터

C# 6은 when을 추가했습니다. catch 절이 적용될지를 정하는 조건입니다. false이면 런타임은 그 절이 없었던 것처럼 다른 처리기를 계속 찾습니다.

출력:

Not found: show an empty page
Server error 503: retry later
Unhandled by Call: HTTP 401

필터를 쓰면 잡았다가 다시 던지지 않고도 예외 안의 데이터로 분기할 수 있습니다. 세 번째 절처럼 서로 관련 없는 두 타입을 똑같이 처리하는 깔끔한 방법이기도 합니다. 필터는 스택이 풀리기 전에 실행되므로, 일치하는 필터가 없을 때 디버거나 크래시 덤프가 여전히 원래 상태를 보여 줍니다.

finally: 항상 실행되는 코드

finally 블록은 제어가 try를 떠날 때 실행됩니다. 끝났든, 일찍 반환했든, 예외를 던졌든 마찬가지입니다. 정리 코드가 들어가는 곳입니다. (한 가지 단서가 있습니다. 어디서도 예외를 잡지 않으면 프로세스가 이를 실행하지 않고 끝날 수 있습니다.)

출력:

Open connection
Close connection
finished
Open connection
Close connection
returned early
Open connection
Close connection
handled error

모든 경우에 메서드의 결과가 Main에 도달하기 전에 "Close connection"이 출력됩니다. finally는 return 값이 계산된 뒤, 메서드가 실제로 반환되기 전에 실행됩니다. try는 catch 없이 finally만 가질 수도 있으며, 예외가 호출하는 쪽으로 계속 가게 두면서 정리합니다.

IDisposable을 구현하는 객체(파일, 스트림, 연결)에는 using 문이 이 try/finally를 대신 작성해 줍니다.

다시 던지기: throw;와 throw e;

때로 catch 블록은 무언가를 기록한 뒤 예외가 계속 가게 둡니다. 어떻게 다시 던지느냐가 스택 추적이 살아남는지를 정합니다:

출력:

throw;   trace mentions LoadConfig: True
throw e; trace mentions LoadConfig: False

throw e;는 예외를 catch 블록에서 새로 던진 것으로 취급하므로, 오류가 일어난 메서드를 포함해 그 아래 프레임들이 추적에서 사라집니다. 항상 맨 throw;로 다시 던지세요. (NoInlining 특성은 JIT이 이렇게 작은 메서드를 호출자에 합쳐서 두 추적 모두에서 숨길 수 있기 때문에 있는 것뿐입니다.)

대신 맥락을 더하려면 예외를 새 예외로 감싸고 원래 예외를 내부 예외로 넘기세요: throw new ConfigException("Could not start the app", e);. 내부 예외는 자기 스택 추적을 유지하고, 로거는 연쇄를 출력합니다. 직접 예외 타입을 작성하는 방법은 예외 던지기에서 다룹니다.

Exception 객체

모든 예외는 System.Exception에서 파생됩니다. 가장 많이 쓰는 멤버:

멤버담고 있는 것
Message사람이 읽을 수 있는 설명
GetType().NameFormatException 같은 예외 타입
StackTracethrow 지점의 메서드 호출 연쇄
InnerException이 예외를 일으킨 예외, 또는 null
ToString()타입, 메시지, 내부 예외, 스택 추적 전체

나중에 실패를 진단하려면 e.Message 대신 e.ToString()을 기록하세요. 메시지만으로는 문제가 어디서 났는지 거의 알 수 없습니다. 최종 사용자에게 e.ToString()을 보여 주지는 마세요.

흔한 예외 타입

예외전형적인 원인
NullReferenceExceptionnull 참조에서 멤버 호출
ArgumentNullException값이 필요한 메서드에 null을 넘김
ArgumentOutOfRangeException인수나 리스트 인덱스가 허용 범위 밖
IndexOutOfRangeException배열 인덱스가 경계 밖
FormatException형식이 잘못된 텍스트에 대한 int.Parse, DateTime.Parse 등
InvalidCastException객체가 아닌 타입으로의 명시적 캐스트
InvalidOperationException호출하기에 객체 상태가 잘못됨(빈 시퀀스, 수정된 컬렉션)
KeyNotFoundException인덱서로 없는 딕셔너리 키 읽기
DivideByZeroException정수나 decimal을 0으로 나눔
OverflowException파싱한 숫자, checked 변환, checked 산술 결과가 타입에 담기지 않음
FileNotFoundException, IOException파일 시스템 문제

NullReferenceException, IndexOutOfRangeException, InvalidCastException은 거의 항상 버그를 뜻합니다. 잡지 말고 코드를 고치세요.

예외를 삼키지 않기

빈 catch는 예상하지 못한 것을 포함해 모든 오류를 숨깁니다:

try
{
    SaveOrder(order);
}
catch (Exception)
{
    // nothing: the order silently was not saved
}

프로그램은 저장이 성공한 것처럼 계속 실행되고 진짜 원인은 사라집니다. 예외 처리를 정직하게 유지하는 지침:

  • 처리할 수 있는 것만, 처리할 수 있는 수준에서 잡으세요.
  • 기록하려고 잡았다면, 프로그램이 정말로 계속할 수 있는 게 아닌 한 throw;로 다시 던지세요.
  • Exception은 바깥 가장자리에서만 잡으세요: Main, 요청 처리기, 작업자 루프.
  • 예상된 경우에는 예외보다 TryParse, TryGetValue, null 검사를 우선 쓰세요. 예외를 던지는 것은 검사에 비해 느리지만, 아무것도 던지지 않는 try 블록은 비용이 거의 없습니다.

흔한 실수

  • catch (Exception)을 먼저 두기. 뒤의 절이 도달할 수 없게 됩니다(CS0160).
  • 다시 던질 때 throw e;. 원래 스택 추적을 지웁니다. throw;를 쓰세요.
  • 빈 catch 블록. 오류가 사라집니다. 최소한 기록하고 다시 던지세요.
  • 제어 흐름에 예외 쓰기. FormatException을 잡지 말고 TryParse로 입력을 검증하세요.
  • 프레임워크 예외의 e.Message를 사용자에게 보여 주기. 문구가 .NET 버전마다 다르고 개발자를 위해 쓰였습니다.

자주 묻는 질문

C#에서 try catch는 어떻게 동작하나요?

실패할 수 있는 코드는 try 블록에 둡니다. 거기서 어떤 문장이 예외를 던지면 블록의 나머지는 건너뛰고, 런타임은 먼저 현재 메서드에서, 그다음 각 호출자에서 예외와 타입이 맞는 catch 절을 찾습니다. 처음 일치하는 catch가 실행되고, 실행은 try 문 전체 뒤에서 이어집니다.

C#에서 finally는 항상 실행되나요?

거의 항상 그렇습니다. try 블록이 정상적으로 끝난 뒤, catch가 예외를 처리한 뒤, 블록 안의 return이나 break 뒤, 그리고 예외가 호출 스택 위쪽의 catch로 가는 도중 지나갈 때 실행됩니다. 프로세스가 먼저 끝나면 실행되지 않습니다. 강제 종료된 프로세스, Environment.FailFast, StackOverflowException, 그리고 .NET Core 이상에서 아무도 잡지 않는 예외는 finally 블록이 실행되기 전에 프로세스를 끝냅니다.

C#에서 여러 예외는 어떻게 잡나요?

가장 구체적인 타입부터 여러 catch 절을 씁니다: catch (IOException) 앞에 catch (FileNotFoundException), 그 뒤에 catch (Exception). 앞의 절이 이미 그 타입을 잡아서 도달할 수 없는 절은 컴파일러가 거부합니다. 서로 관련 없는 두 타입을 같은 방식으로 처리하려면 필터를 쓰세요: catch (Exception e) when (e is FormatException || e is OverflowException).

C#에서 throw와 throw ex의 차이는 무엇인가요?

catch 안에서 throw;는 현재 예외를 원래 스택 추적과 함께 다시 던집니다. throw ex;는 같은 객체를 던지지만 스택 추적을 현재 줄로 초기화하므로, 오류가 실제로 일어난 메서드가 추적에서 사라집니다. throw;를 쓰거나 감싸세요: throw new MyException("context", ex);.

C#의 예외 필터란 무엇인가요?

catch 뒤의 when 절입니다(C# 6 이상): catch (HttpRequestException e) when (e.Message.Contains("404")). 조건이 true일 때만 catch 블록이 실행되고, 아니면 예외는 그 절이 없었던 것처럼 다른 처리기를 계속 찾으며, 스택은 풀리지 않습니다.

C#에서 Exception을 잡아도 되나요?

프로그램의 가장자리에서만 그렇습니다. Main의 맨 위, 요청 처리기, 백그라운드 루프처럼 오류를 기록하고 계속하거나 깔끔하게 종료하는 것이 할 일인 곳입니다. 코드 깊은 곳에서는 실제로 처리할 수 있는 특정 타입만 잡고, 나머지는 모두 전파되게 두세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기