Repository: CloudyKit/jet
Pull request #152 (2020-06-23 14:08:39)

Introduce try/catch control structure
`{{try}} .. {{end}}` or `{{try}} .. {{catch [err]}} .. {{end}}` executes a list of statements, recovering from any panic (runtime errors) inside. When using try with catch, the statement list inside catch/end is executed in the event of a recovery.

catch takes an optional identifier name under which the error will be available inside the catch statement list.

In case of no runtime error, the output generated by the try statement list will be kept. In case of any panic, no output from the try statement list will be kept.

Born out of the ashes of #151.

tooolbox: Brilliant!  (I didn't deep-dive into the internals to review what you did but the concept is great.)

Do you think it will affect performance much?  Probably only if you're dealing with tons of panics, I imagine.
sauerbraten: > Do you think it will affect performance much?

Well it only impacts performance when you actually use it. If you use it, there's certainly a hit to the speed of outputting rendered results and in increase in memory consumption, because we buffer output until the entire block was executed. Other than that, it shouldn't have a big impact, it only shuffles around a few variables before and after executing the statement list. Deferred functions don't have any significant overhead in recent Go versions, either, so the panic handling should be ok, too.
sauerbraten: I liked the suggestion to rename "recover" to "catch" you had here: https://github.com/CloudyKit/jet/pull/151#issuecomment-648292731. Coming up!
tooolbox: > Well it only impacts performance when you actually use it. If you use it, there's certainly a hit to the speed of outputting rendered results and an increase in memory consumption, because we buffer output until the entire block was executed. Other than that, it shouldn't have a big impact, it only shuffles around a few variables before and after executing the statement list. Deferred functions don't have any significant overhead in recent Go versions, either, so the panic handling should be ok, too.

That makes sense.  Yeah, it's something you can use if you need it.

> I liked the suggestion to rename "recover" to "catch" you had here: #151 (comment). Coming up!

👍   I think this is great, a good tool for the arsenal :)

Patch:
diff --git a/constructors.go b/constructors.go
index e8364ee..20187fd 100644
--- a/constructors.go
+++ b/constructors.go
@@ -130,14 +130,22 @@ func (t *Template) newYield(pos Pos, line int, name string, bplist *BlockParamet
 	return &YieldNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeYield, Pos: pos, Line: line}, Name: name, Parameters: bplist, Expression: pipe, Content: content, IsContent: isContent}
 }
 
-func (t *Template) newInclude(pos Pos, line int, name, pipe Expression) *IncludeNode {
-	return &IncludeNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeInclude, Pos: pos, Line: line}, Name: name, Expression: pipe}
+func (t *Template) newInclude(pos Pos, line int, name, context Expression) *IncludeNode {
+	return &IncludeNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeInclude, Pos: pos, Line: line}, Name: name, Context: context}
 }
 
 func (t *Template) newReturn(pos Pos, line int, pipe Expression) *ReturnNode {
 	return &ReturnNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeReturn, Pos: pos, Line: line}, Value: pipe}
 }
 
+func (t *Template) newTry(pos Pos, line int, list *ListNode, catch *catchNode) *TryNode {
+	return &TryNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeTry, Pos: pos, Line: line}, List: list, Catch: catch}
+}
+
+func (t *Template) newCatch(pos Pos, line int, errVar *IdentifierNode, list *ListNode) *catchNode {
+	return &catchNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: nodeCatch, Pos: pos, Line: line}, Err: errVar, List: list}
+}
+
 func (t *Template) newNumber(pos Pos, text string, typ itemType) (*NumberNode, error) {
 	n := &NumberNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeNumber, Pos: pos}, Text: text}
 	// todo: optimize
diff --git a/eval.go b/eval.go
index d947f3f..d722410 100644
--- a/eval.go
+++ b/eval.go
@@ -15,6 +15,7 @@
 package jet
 
 import (
+	"bytes"
 	"fmt"
 	"io"
 	"reflect"
@@ -130,78 +131,94 @@ func (st *scope) getBlock(name string) (block *BlockNode, has bool) {
 	return
 }
 
-// Set sets variable ${name} in the current template scope
-func (state *Runtime) Set(name string, val interface{}) {
-	state.setValue(name, reflect.ValueOf(val))
+func (state *Runtime) setValue(name string, val reflect.Value) error {
+	// try changing existing variable in current or parent scope
+	sc := state.scope
+	for sc != nil {
+		if _, ok := sc.variables[name]; ok {
+			sc.variables[name] = val
+			return nil
+		}
+		sc = sc.parent
+	}
+
+	return fmt.Errorf("could not assign %q = %v because variable %q is uninitialised", name, val, name)
 }
 
-func (state *Runtime) setValue(name string, val reflect.Value) bool {
+// SetGlobal sets variable ${name} in the top-most template scope
+func (state *Runtime) SetGlobal(name string, val interface{}) {
 	sc := state.scope
-	initial := sc
 
-	// try to resolve variables in the current scope
-	_, ok := sc.variables[name]
-
-	// if not found walks parent scopes
-	for !ok && sc.parent != nil {
+	// walk up to top-most valid scope
+	for sc.parent != nil && sc.parent.variables != nil {
 		sc = sc.parent
-		_, ok = sc.variables[name]
 	}
 
-	if ok {
-		sc.variables[name] = val
-		return false
-	}
+	sc.variables[name] = reflect.ValueOf(val)
+}
 
-	for initial.variables == nil && initial.parent != nil {
-		initial = initial.parent
-	}
+// Set sets the existing variable ${name} in the template scope it lives in.
+func (state *Runtime) Set(name string, val interface{}) error {
+	return state.setValue(name, reflect.ValueOf(val))
+}
 
-	if initial.variables != nil {
-		sc.variables[name] = val
-		return false
-	}
-	return true
+// Let creates a variable ${name} in the current template scope (possibly shadowing an existing variable of the same name in a parent scope).
+func (state *Runtime) Let(name string, val interface{}) {
+	state.scope.variables[name] = reflect.ValueOf(val)
 }
 
-// Resolve resolves a value from the execution context
-func (state *Runtime) Resolve(name string) (reflect.Value, error) {
-	v, ok := reflect.Value{}, false
-	defer func() { v = indirectEface(v) }()
+// SetOrLet calls Set() (if a variable with the given name is visible from the current scope) or Let() (if there is no variable with the given name in the current or any parent scope).
+func (state *Runtime) SetOrLet(name string, val interface{}) {
+	_, err := state.resolve(name)
+	if err != nil {
+		state.Let(name, val)
+	} else {
+		state.Set(name, val)
+	}
+}
 
+// Resolve resolves a value from the execution context.
+func (state *Runtime) resolve(name string) (reflect.Value, error) {
 	if name == "." {
-		return state.context, nil // indirectEface not needed
+		return state.context, nil
 	}
 
 	// try current, then parent variable scopes
 	sc := state.scope
 	for sc != nil {
-		v, ok = sc.variables[name]
+		v, ok := sc.variables[name]
 		if ok {
-			return v, nil
+			return indirectEface(v), nil
 		}
 		sc = sc.parent
 	}
 
 	// try globals
 	state.set.gmx.RLock()
-	v, ok = state.set.globals[name]
+	v, ok := state.set.globals[name]
 	state.set.gmx.RUnlock()
 	if ok {
-		return v, nil
+		return indirectEface(v), nil
 	}
 
 	// try default variables
 	v, ok = defaultVariables[name]
 	if ok {
-		return v, nil
+		return indirectEface(v), nil
 	}
 
-	return reflect.Value{}, fmt.Errorf("identifier '%q' not available in current (%+v) or parent scope, global, or default variables", name, state.scope.variables)
+	return reflect.Value{}, fmt.Errorf("identifier %q not available in current (%+v) or parent scope, global, or default variables", name, state.scope.variables)
+}
+
+// Resolve calls resolve() and ignores any errors, meaning it may return a zero reflect.Value.
+func (state *Runtime) Resolve(name string) reflect.Value {
+	v, _ := state.resolve(name)
+	return v
 }
 
+// Resolve calls resolve() and panics if there is an error.
 func (state *Runtime) MustResolve(name string) reflect.Value {
-	v, err := state.Resolve(name)
+	v, err := state.resolve(name)
 	if err != nil {
 		panic(err)
 	}
@@ -214,12 +231,12 @@ func (st *Runtime) recover(err *error) {
 	st.context = reflect.Value{}
 	pool_State.Put(st)
 	if recovered := recover(); recovered != nil {
-		var is bool
-		if _, is = recovered.(runtime.Error); is {
+		var ok bool
+		if _, ok = recovered.(runtime.Error); ok {
 			panic(recovered)
 		}
-		*err, is = recovered.(error)
-		if !is {
+		*err, ok = recovered.(error)
+		if !ok {
 			panic(recovered)
 		}
 	}
@@ -228,7 +245,10 @@ func (st *Runtime) recover(err *error) {
 func (st *Runtime) executeSet(left Expression, right reflect.Value) {
 	typ := left.Type()
 	if typ == NodeIdentifier {
-		st.setValue(left.(*IdentifierNode).Ident, right)
+		err := st.setValue(left.(*IdentifierNode).Ident, right)
+		if err != nil {
+			left.error(err)
+		}
 		return
 	}
 	var value reflect.Value
@@ -481,6 +501,9 @@ func (st *Runtime) executeList(list *ListNode) (returnValue reflect.Value) {
 			if isLet {
 				st.releaseScope()
 			}
+		case NodeTry:
+			node := node.(*TryNode)
+			returnValue = st.executeTry(node)
 		case NodeYield:
 			node := node.(*YieldNode)
 			if node.IsContent {
@@ -513,6 +536,39 @@ func (st *Runtime) executeList(list *ListNode) (returnValue reflect.Value) {
 	return returnValue
 }
 
+func (st *Runtime) executeTry(try *TryNode) (returnValue reflect.Value) {
+	writer := st.Writer
+	buf := new(bytes.Buffer)
+
+	defer func() {
+		r := recover()
+
+		// copy buffered render output to writer only if no panic occured
+		if r == nil {
+			io.Copy(writer, buf)
+		} else {
+			// st.Writer is already set to its original value since the later defer ran first
+			if try.Catch != nil {
+				if try.Catch.Err != nil {
+					st.newScope()
+					st.scope.variables[try.Catch.Err.Ident] = reflect.ValueOf(r)
+				}
+				if try.Catch.List != nil {
+					returnValue = st.executeList(try.Catch.List)
+				}
+				if try.Catch.Err != nil {
+					st.releaseScope()
+				}
+			}
+		}
+	}()
+
+	st.Writer = buf
+	defer func() { st.Writer = writer }()
+
+	return st.executeList(try.List)
+}
+
 func (st *Runtime) executeInclude(node *IncludeNode) (returnValue reflect.Value) {
 	var templateName string
 	name := st.evalPrimaryExpressionGroup(node.Name)
@@ -536,10 +592,10 @@ func (st *Runtime) executeInclude(node *IncludeNode) (returnValue reflect.Value)
 	st.blocks = t.processedBlocks
 
 	var context reflect.Value
-	if node.Expression != nil {
+	if node.Context != nil {
 		context = st.context
 		defer func() { st.context = context }()
-		st.context = st.evalPrimaryExpressionGroup(node.Expression)
+		st.context = st.evalPrimaryExpressionGroup(node.Context)
 	}
 
 	Root := t.Root
@@ -548,9 +604,7 @@ func (st *Runtime) executeInclude(node *IncludeNode) (returnValue reflect.Value)
 		Root = t.Root
 	}
 
-	returnValue = st.executeList(Root)
-
-	return returnValue
+	return st.executeList(Root)
 }
 
 var (
@@ -662,7 +716,7 @@ func (st *Runtime) isSet(node Node) (ok bool) {
 		resolved, err := resolveIndex(base, index)
 		return err == nil && notNil(resolved)
 	case NodeIdentifier:
-		value, err := st.Resolve(node.String())
+		value, err := st.resolve(node.String())
 		return err == nil && notNil(value)
 	case NodeField:
 		node := node.(*FieldNode)
@@ -1023,7 +1077,7 @@ func (st *Runtime) evalBaseExpressionGroup(node Node) reflect.Value {
 	case NodeString:
 		return reflect.ValueOf(&node.(*StringNode).Text).Elem()
 	case NodeIdentifier:
-		resolved, err := st.Resolve(node.(*IdentifierNode).Ident)
+		resolved, err := st.resolve(node.(*IdentifierNode).Ident)
 		if err != nil {
 			node.error(err)
 		}
diff --git a/lex.go b/lex.go
index 4b9ce39..e473301 100644
--- a/lex.go
+++ b/lex.go
@@ -85,20 +85,22 @@ const (
 	// Keywords appear after all the rest.
 	itemKeyword // used only to delimit the keywords
 	itemExtends
+	itemImport
+	itemInclude
 	itemBlock
+	itemEnd
 	itemYield
-	itemReturn
 	itemContent
-	itemInclude
-	itemElse
-	itemEnd
 	itemIf
-	itemNil
+	itemElse
 	itemRange
-	itemImport
+	itemTry
+	itemCatch
+	itemReturn
 	itemAnd
 	itemOr
 	itemNot
+	itemNil
 	itemMSG
 	itemTrans
 )
@@ -109,22 +111,28 @@ var key = map[string]itemType{
 
 	"include": itemInclude,
 	"block":   itemBlock,
+	"end":     itemEnd,
 	"yield":   itemYield,
-	"return":  itemReturn,
+	"content": itemContent,
 
-	"else": itemElse,
-	"end":  itemEnd,
 	"if":   itemIf,
+	"else": itemElse,
 
 	"range": itemRange,
-	"nil":   itemNil,
-	"and":   itemAnd,
-	"or":    itemOr,
-	"not":   itemNot,
 
-	"content": itemContent,
-	"msg":     itemMSG,
-	"trans":   itemTrans,
+	"try":   itemTry,
+	"catch": itemCatch,
+
+	"return": itemReturn,
+
+	"and": itemAnd,
+	"or":  itemOr,
+	"not": itemNot,
+
+	"nil": itemNil,
+
+	"msg":   itemMSG,
+	"trans": itemTrans,
 }
 
 const eof = -1
diff --git a/node.go b/node.go
index df22cc9..d9c29b5 100644
--- a/node.go
+++ b/node.go
@@ -75,20 +75,22 @@ const (
 	NodeAction                     //A non-control action such as a field evaluation.
 	NodeChain                      //A sequence of field accesses.
 	NodeCommand                    //An element of a pipeline.
-	nodeElse                       //An else action. Not added to tree.
-	nodeEnd                        //An end action. Not added to tree.
 	NodeField                      //A field or method name.
 	NodeIdentifier                 //An identifier; always a function name.
-	NodeIf                         //An if action.
 	NodeList                       //A list of Nodes.
 	NodePipe                       //A pipeline of commands.
-	NodeRange                      //A range action.
-	nodeContent
+	NodeSet
 	//NodeWith                       //A with action.
-	NodeBlock
 	NodeInclude
+	NodeBlock
+	nodeEnd //An end action. Not added to tree.
 	NodeYield
-	NodeSet
+	nodeContent
+	NodeIf    //An if action.
+	nodeElse  //An else action. Not added to tree.
+	NodeRange //A range action.
+	NodeTry
+	nodeCatch
 	NodeReturn
 	beginExpressions
 	NodeString //A string constant.
@@ -546,15 +548,15 @@ func (t *YieldNode) String() string {
 // IncludeNode represents a {{include }} action.
 type IncludeNode struct {
 	NodeBase
-	Name       Expression
-	Expression Expression
+	Name    Expression
+	Context Expression
 }
 
 func (t *IncludeNode) String() string {
-	if t.Expression == nil {
+	if t.Context == nil {
 		return fmt.Sprintf("{{include %s}}", t.Name)
 	}
-	return fmt.Sprintf("{{include %s %s}}", t.Name, t.Expression)
+	return fmt.Sprintf("{{include %s %s}}", t.Name, t.Context)
 }
 
 type binaryExprNode struct {
@@ -674,3 +676,26 @@ type ReturnNode struct {
 func (n *ReturnNode) String() string {
 	return fmt.Sprintf("return %v", n.Value)
 }
+
+type TryNode struct {
+	NodeBase
+	List  *ListNode
+	Catch *catchNode
+}
+
+func (n *TryNode) String() string {
+	if n.Catch != nil {
+		return fmt.Sprintf("{{try}}%s%s", n.List, n.Catch)
+	}
+	return fmt.Sprintf("{{try}}%s{{end}}", n.List)
+}
+
+type catchNode struct {
+	NodeBase
+	Err  *IdentifierNode
+	List *ListNode
+}
+
+func (n *catchNode) String() string {
+	return fmt.Sprintf("{{catch %s}}%s{{end}}", n.Err, n.List)
+}
diff --git a/parse.go b/parse.go
index 59a91d8..4be44aa 100644
--- a/parse.go
+++ b/parse.go
@@ -347,16 +347,11 @@ func (t *Template) parseBlock() Node {
 
 	t.expect(itemRightDelim, context)
 
-	list, end := t.itemList()
+	list, end := t.itemList(nodeContent, nodeEnd)
 	var contentList *ListNode
 
 	if end.Type() == nodeContent {
-		contentList, end = t.itemList()
-		if end.Type() != nodeEnd {
-			t.errorf("unexpected %s in %s", end, context)
-		}
-	} else if end.Type() != nodeEnd {
-		t.errorf("unexpected %s in %s", end, context)
+		contentList, end = t.itemList(nodeEnd)
 	}
 
 	block := t.newBlock(name.pos, t.lex.lineNumber(), name.val, bplist, pipe, list, contentList)
@@ -372,7 +367,6 @@ func (t *Template) parseYield() Node {
 		name    item
 		bplist  *BlockParameterList
 		content *ListNode
-		end     Node
 	)
 
 	// parse block name
@@ -407,10 +401,7 @@ func (t *Template) parseYield() Node {
 			// parse content from following nodes (until {{end}})
 			t.nextNonSpace()
 			t.expect(itemRightDelim, context)
-			content, end = t.itemList()
-			if end.Type() != nodeEnd {
-				t.errorf("unexpected %s in %s", end, context)
-			}
+			content, _ = t.itemList(nodeEnd)
 		} else {
 			t.unexpected(t.nextNonSpace(), context)
 		}
@@ -420,17 +411,13 @@ func (t *Template) parseYield() Node {
 }
 
 func (t *Template) parseInclude() Node {
-	var pipe Expression
-
+	var context Expression
 	name := t.expression("include")
-
 	if t.peekNonSpace().typ != itemRightDelim {
-		pipe = t.expression("include")
+		context = t.expression("include")
 	}
-
 	t.expect(itemRightDelim, "include invocation")
-
-	return t.newInclude(name.Position(), t.lex.lineNumber(), name, pipe)
+	return t.newInclude(name.Position(), t.lex.lineNumber(), name, context)
 }
 
 func (t *Template) parseReturn() Node {
@@ -439,33 +426,17 @@ func (t *Template) parseReturn() Node {
 	return t.newReturn(value.Position(), t.lex.lineNumber(), value)
 }
 
-// itemListBlock:
-//	textOrAction*
-// Terminates at {{end}} or {{else}}, returned separately.
-func (t *Template) itemListBlock() (list *ListNode, next Node) {
-	list = t.newList(t.peekNonSpace().pos)
-	for t.peekNonSpace().typ != itemEOF {
-		n := t.textOrAction()
-		switch n.Type() {
-		case nodeEnd, nodeContent:
-			return list, n
-		}
-		list.append(n)
-	}
-	t.errorf("unexpected EOF")
-	return
-}
-
-// itemListControl:
+// itemList:
 //	textOrAction*
-// Terminates at {{end}}, returned separately.
-func (t *Template) itemList() (list *ListNode, next Node) {
+// Terminates at any of the given nodes, returned separately.
+func (t *Template) itemList(terminatedBy ...NodeType) (list *ListNode, next Node) {
 	list = t.newList(t.peekNonSpace().pos)
 	for t.peekNonSpace().typ != itemEOF {
 		n := t.textOrAction()
-		switch n.Type() {
-		case nodeEnd, nodeElse, nodeContent:
-			return list, n
+		for _, terminatorType := range terminatedBy {
+			if n.Type() == terminatorType {
+				return list, n
+			}
 		}
 		list.append(n)
 	}
@@ -489,22 +460,26 @@ func (t *Template) textOrAction() Node {
 
 func (t *Template) action() (n Node) {
 	switch token := t.nextNonSpace(); token.typ {
-	case itemElse:
-		return t.elseControl()
+	case itemInclude:
+		return t.parseInclude()
+	case itemBlock:
+		return t.parseBlock()
 	case itemEnd:
 		return t.endControl()
+	case itemYield:
+		return t.parseYield()
 	case itemContent:
 		return t.contentControl()
 	case itemIf:
 		return t.ifControl()
+	case itemElse:
+		return t.elseControl()
 	case itemRange:
 		return t.rangeControl()
-	case itemBlock:
-		return t.parseBlock()
-	case itemInclude:
-		return t.parseInclude()
-	case itemYield:
-		return t.parseYield()
+	case itemTry:
+		return t.parseTry()
+	case itemCatch:
+		return t.parseCatch()
 	case itemReturn:
 		return t.parseReturn()
 	}
@@ -887,11 +862,9 @@ func (t *Template) parseControl(allowElseIf bool, context string) (pos Pos, line
 
 	t.expect(itemRightDelim, context)
 	var next Node
-	list, next = t.itemList()
-	switch next.Type() {
-	case nodeEnd: //done
-	case nodeElse:
-		if allowElseIf {
+	list, next = t.itemList(nodeElse, nodeEnd)
+	if next.Type() == nodeElse {
+		if allowElseIf && t.peek().typ == itemIf {
 			// Special case for "else if". If the "else" is followed immediately by an "if",
 			// the elseControl will have left the "if" token pending. Treat
 			//	{{if a}}_{{else if b}}_{{end}}
@@ -899,17 +872,12 @@ func (t *Template) parseControl(allowElseIf bool, context string) (pos Pos, line
 			//	{{if a}}_{{else}}{{if b}}_{{end}}{{end}}.
 			// To do this, parse the if as usual and stop at it {{end}}; the subsequent{{end}}
 			// is assumed. This technique works even for long if-else-if chains.
-			if t.peek().typ == itemIf {
-				t.next() // Consume the "if" token.
-				elseList = t.newList(next.Position())
-				elseList.append(t.ifControl())
-				// Do not consume the next item - only one {{end}} required.
-				break
-			}
-		}
-		elseList, next = t.itemList()
-		if next.Type() != nodeEnd {
-			t.errorf("expected end; found %s", next)
+			t.next() // Consume the "if" token.
+			elseList = t.newList(next.Position())
+			elseList.append(t.ifControl())
+			// Do not consume the next item - only one {{end}} required.
+		} else {
+			elseList, next = t.itemList(nodeEnd)
 		}
 	}
 	return pos, line, set, expression, list, elseList
@@ -958,6 +926,46 @@ func (t *Template) elseControl() Node {
 	return t.newElse(t.expect(itemRightDelim, "else").pos, t.lex.lineNumber())
 }
 
+// Try-catch:
+//	{{try}}
+//    itemList
+//  {{catch <ident>}}
+//    itemList
+//  {{end}}
+// try keyword is past.
+func (t *Template) parseTry() *TryNode {
+	var recov *catchNode
+	line := t.lex.lineNumber()
+	pos := t.expect(itemRightDelim, "try").pos
+	list, next := t.itemList(nodeCatch, nodeEnd)
+	if next.Type() == nodeCatch {
+		recov = next.(*catchNode)
+	}
+
+	return t.newTry(pos, line, list, recov)
+}
+
+// catch:
+//  {{catch <ident>}}
+//    itemList
+//  {{end}}
+// catch keyword is past.
+func (t *Template) parseCatch() *catchNode {
+	line := t.lex.lineNumber()
+	var errVar *IdentifierNode
+	peek := t.peekNonSpace()
+	if peek.typ != itemRightDelim {
+		_errVar := t.term()
+		if typ := _errVar.Type(); typ != NodeIdentifier {
+			t.errorf("unexpected node type '%s' in catch", typ)
+		}
+		errVar = _errVar.(*IdentifierNode)
+	}
+	t.expect(itemRightDelim, "catch")
+	list, _ := t.itemList(nodeEnd)
+	return t.newCatch(peek.pos, line, errVar, list)
+}
+
 // term:
 //	literal (number, string, nil, boolean)
 //	function (identifier)


Test patch:
diff --git a/eval_test.go b/eval_test.go
index 9015134..a803312 100644
--- a/eval_test.go
+++ b/eval_test.go
@@ -634,6 +634,14 @@ func TestExecReturn(t *testing.T) {
 	RunJetTestWithSet(t, set, nil, nil, "test_in_include", "", "from inside included template\n")
 }
 
+func TestTryCatch(t *testing.T) {
+	set := NewHTMLSet("./testData/tryCatch")
+	RunJetTestWithSet(t, set, nil, nil, "try", "", "before try without panic ...\n\nsome content\n\nfoo\n\nafter try without panic ...\nbefore panic ...\n\nafter panic ...")
+	RunJetTestWithSet(t, set, nil, nil, "try_catch", "", "before panic ...\n\nan error occured!\n\nafter panic ...")
+	RunJetTestWithSet(t, set, nil, nil, "try_catch_err", "", "before panic ...\n\nan error occured: Jet Runtime Error (&#34;try_catch_err.jet&#34;:3): identifier &#34;undefined_identifier_that_causes_panic&#34; not available in current (map[]) or parent scope, global, or default variables\n\nafter panic ...")
+	RunJetTestWithSet(t, set, nil, nil, "try_include", "", "before broken include ...\n\nafter broken include ...")
+}
+
 func BenchmarkSimpleAction(b *testing.B) {
 	t, _ := JetTestingSet.GetTemplate("actionNode_dummy")
 	for i := 0; i < b.N; i++ {
diff --git a/testData/tryCatch/panic.jet b/testData/tryCatch/panic.jet
new file mode 100644
index 0000000..927f841
--- /dev/null
+++ b/testData/tryCatch/panic.jet
@@ -0,0 +1,2 @@
+some content that won't appear in the output, because the next line will panic ...
+{{ undefined_identifier_that_causes_panic }}
\ No newline at end of file
diff --git a/testData/tryCatch/try.jet b/testData/tryCatch/try.jet
new file mode 100644
index 0000000..d14f0fe
--- /dev/null
+++ b/testData/tryCatch/try.jet
@@ -0,0 +1,13 @@
+before try without panic ...
+{{try}}
+some content
+{{foo := "foo"}}
+{{foo}}
+{{end}}
+after try without panic ...
+before panic ...
+{{try}}
+some content that will not be rendered because the next line panics ...
+{{undefined_identifier_that_causes_panic}}
+{{end}}
+after panic ...
\ No newline at end of file
diff --git a/testData/tryCatch/try_catch.jet b/testData/tryCatch/try_catch.jet
new file mode 100644
index 0000000..0605bad
--- /dev/null
+++ b/testData/tryCatch/try_catch.jet
@@ -0,0 +1,7 @@
+before panic ...
+{{try}}
+{{undefined_identifier_that_causes_panic}}
+{{catch}}
+an error occured!
+{{end}}
+after panic ...
\ No newline at end of file
diff --git a/testData/tryCatch/try_catch_err.jet b/testData/tryCatch/try_catch_err.jet
new file mode 100644
index 0000000..f6b73f6
--- /dev/null
+++ b/testData/tryCatch/try_catch_err.jet
@@ -0,0 +1,7 @@
+before panic ...
+{{try}}
+{{undefined_identifier_that_causes_panic}}
+{{catch err}}
+an error occured: {{err}}
+{{end}}
+after panic ...
\ No newline at end of file
diff --git a/testData/tryCatch/try_include.jet b/testData/tryCatch/try_include.jet
new file mode 100644
index 0000000..9f2d55f
--- /dev/null
+++ b/testData/tryCatch/try_include.jet
@@ -0,0 +1,5 @@
+before broken include ...
+{{try}}
+{{include "panic.jet"}}
+{{end}}
+after broken include ...
\ No newline at end of file