JS 제너레이터에서 throw/catch

Jooyung Han (한주영)
4 min readJan 15, 2018

--

대칭 코루틴으로 동작하는 경우에는 throw/catch가 성립하지 않는다. throw/catch는 콜스택 되감기(unwinding) 기능인데 대칭 코루틴에서 컨텍스트 스위칭되는 것은 콜스택과 무관한 점프이기 때문. 하지만 호출관계는 있지만 스택오버플로를 회피하기 위해 제너레이터 코루틴을 사용하는 경우라면 얘기가 다르다.

foo() 함수가 bar() 함수를 부르는 다음 경우를 보자.

function foo() {
return 1 + bar();
}
function bar() {
return 42;
}
console.log(foo());

foo>bar 로 이어지는 호출관계가 있다. 제너레이터를 이용하면 스택리스하게 이 호출관계를 나타낼 수 있다.

function* foo() {
return 1 + (yield bar());
}
function* bar() {
return 42;
}
console.log(loop(foo()));

내용은 동일하지만 실행 시 콜스택 사용이 다르다. 제너레이터 버전에서는 yield 할때 foo() 에서 빠져나와 loop()에서 bar() 로 진입한다. bar() 에서 42가 반환되면 loop() 에서 이 값을 foo() 로 전달하며 진입한다. 다시 foo()가 43을 반환하면 loop() 는 이 값을 반환한다.

call      |  generator
------------------------
foo() | loop()
bar() | foo()
foo() | loop()
| bar()
| loop()
| foo()
| loop()

loop() 구현은 다음과 같다.

function loop(g) {
const stack = [g];
let returnValue;
while (stack.length > 0) {
let { value, done } = stack[stack.length - 1].next(returnValue);
returnValue = undefined;
if (done) {
stack.pop();
returnValue = value;
} else {
stack.push(value);
}
}
return returnValue;
}

그런데 이 경우 bar() 에서 예외를 던지면 어떻게 될까? 스택을 사용하지 않다보니 이 예외는 foo() 로 전달되는 대신 콜스택 상 바로 위의 loop() 로 전달된다.

function* foo() {
try { return 1 + (yield bar()); } // 의미없다
catch (e) { return 41; }
}
function* bar() {
throw new Error();
}
Error
at bar (ex.js:6:9)
at bar.next (<anonymous>)
at loop (ex.js:14:51)
at Object.<anonymous> (ex.js:25:13)

JavaScript 의 “Generator” 객체는 Iterator와 같이 next() 메쏘드를 가지면서 추가로 throw() 와 return() 메쏘드를 가진다. 여기서 throw() 를 이용하면 원래의 호출 관계에서 bar() 의 예외를 foo() 에서 받는 것과 같은 효과를 얻을 수 있다.

function loop(g) {
const stack = [g];
let returnValue, err;
while (stack.length > 0) {
try {
let next;
if (err) {
next = stack[stack.length - 1].throw(err);
err = undefined;
}
else {
next = stack[stack.length - 1].next(returnValue);
returnValue = undefined;
}
if (next.done) {
stack.pop();
returnValue = next.value;
} else {
stack.push(next.value);
}
} catch (e) {
stack.pop();
err = e;
}

}
if (err) throw err;
return returnValue;
}

이제 foo() 에서는 bar() 가 throw 한 예외를 catch 할 수 있다. 아쉬운 점은 new Error() 로 생성되는 예외의 stack 속성이 큰 도움을 주지 못하는 것.

--

--

Jooyung Han (한주영)

가끔 함수형 프로그래밍 관련 글을 쓰거나 번역합니다. “개미 수열을 푸는 10가지 방법"이란 책을 썼습니다. https://leanpub.com/programming-look-and-say