try/catch/else

# Alan Plum (6 months ago)

I'm hoping this is the right medium to ask this but I would like to propose a language feature called try/catch/else.

Conditional try/catch seems to be all the rage right now but in many cases the problem it really wants to solve seems to be code like the following:

try {
  const suggestions = await fetchSuggestions();
  showSuggestions(suggestions);
} catch (e) {
  alert('Failed to load suggestions');
  // Oops, we also swallow errors from showSuggestions
}
// now do something else

Having a more fine-grained catch wouldn't necessarily help here because both functions might throw the same kind of error but what we're really interested in is discerning the source of the error. So instead some people resort to something like this:

let suggestions;
try {
  suggestions = await fetchSuggestions();
} catch (e) {
  alert('Failed to load suggestions');
  return;
}
showSuggestions(suggestions);
// now do something else - unless we failed to load

Note how we're forced to add a return to explicitly abort the control flow. Unlike the change from const to let this isn't something an IDE would point out while refactoring, so this actually introduces potential for bugs. If we don't actually want to bail out completely this often leads to noisy status booleans (e.g. didNotThrow) or error-prone checks (e.g. !suggestions is a bug if the async function really didn't return anything).

I'm not sure about other languages but Python has a solution for this by adding an else clause:

let suggestions;
try {
  suggestions = await fetchSuggestions();
} catch (e) {
  alert('Failed to load suggestions');
} else {
  showSuggestions(suggestions);
}
// now do something else

The semantics are pretty straightforward: if the try block did not throw, the else block is executed next (before the finally block if any). If the try block does throw, the else block is ignored (like a conditional catch that doesn't match).

I realise there is some ambiguity in using the else keyword for this (though I can't think of a meaningful opposite of "catch" either). There is also some overlap with conditional try/catch but I think this language feature is useful even if conditional try/catch exists.

# Michael Luder-Rosefield (6 months ago)

Well, since promise.then(/blah/).catch(/blah/) is already a thing, why not stick with that?

try { // blah } then(val) { // blah } catch { // blah } finally { // blah }

I added val to the then since we might want to pass something from try to it, just as promises do. Best way to do that might be for try to act as an expression block and implicitly return the last thing in it.

# Alan Plum (6 months ago)

Sounds good in principle but I think the name is misleading.

Actually try/catch/else is the synchronous equivalent of tryFn().then(elseFn, catchFn) but then has other implications (it returns a new promise and the then method actually takes two functions). Calling it then would imply it's equivalent to tryFn().then(thenFn).catch(catchFn) which is actually just the same as the first example, not the try/catch/else. I also think having the else after the catch makes the intention clearer (just like finally comes after catch and else).

# Claude Pache (6 months ago)

What about the following pattern (labelled block + break)?

processSuggestions: {
    let suggestions;
    try {
      suggestions = await fetchSuggestions();
    } catch (e) {
      alert('Failed to load suggestions');
      break processSuggestions;
    }
    showSuggestions(suggestions);
}
# Mark Miller (6 months ago)

that's nice. Whenever I faced this issue I always turned to the boolean flag variable. But this is strictly better. I expect to use it from now on.

# Isiah Meadows (6 months ago)

I honestly wish engines (read: V8) didn't jave so much issue optimizing that. But yes, it's a very useful pattern.

# Peter van der Zee (6 months ago)

On Thu, Feb 8, 2018 at 10:13 AM, Claude Pache <claude.pache at gmail.com> wrote:

What about the following pattern (labelled block + break)?

processSuggestions: {
    let suggestions;
    try {
      suggestions = await fetchSuggestions();
    } catch (e) {
      alert('Failed to load suggestions');
      break processSuggestions;
    }
    showSuggestions(suggestions);
}

I don't mean to hijack this tread. I'm mostly curious why you opt or even suggest for that over putting it in a function and an early return? Like;

function processSuggestions() {
  let suggestions
  try {
    suggestions = await fetchSuggestions();
  } catch (e) {
    return alert('Failed to load suggestions');
  }
  showSuggestions(suggestions);
}

This is almost identical, especially the way the example was written. I understand the differences, I don't think they're a problem for by far most cases where you'd want this.

That said I wouldn't mind seeing try/catch/else/finally because the pattern(s) above still leads to awkward code in the real world.

One could bikeshed on how "else" implies the attempt ("try") to have failed rather than succeeded. On the other hand making it try/then/catch/finally is also going to be confusing so whatever :)

# Alan Plum (6 months ago)

I think the best argument for having try/catch/else is that it makes it trivial to translate promises into async/await. Consider this:

let result = a()
.then(b, c)
.catch(d);

If we want to translate this 1:1 to try/catch/else in an async function we'll end up with something like this:

try {
  let x, y;
  try {
    x = await a();
  } catch (e) {
    y = await c(e);
  } else {
    y = await b(x);
  }
  return y;
} catch (e) {
  return await d(e);
}

Doing this without else would require one of the workarounds suggested upthread.

# Claude Pache (6 months ago)

Le 9 févr. 2018 à 00:19, Peter van der Zee <ecma at qfox.nl> a écrit :

On Thu, Feb 8, 2018 at 10:13 AM, Claude Pache <claude.pache at gmail.com> wrote:

What about the following pattern (labelled block + break)?

processSuggestions: {
   let suggestions;
   try {
     suggestions = await fetchSuggestions();
   } catch (e) {
     alert('Failed to load suggestions');
     break processSuggestions;
   }
   showSuggestions(suggestions);
}

I don't mean to hijack this tread. I'm mostly curious why you opt or even suggest for that over putting it in a function and an early return? Like;

function processSuggestions() {
 let suggestions
 try {
   suggestions = await fetchSuggestions();
 } catch (e) {
   return alert('Failed to load suggestions');
 }
 showSuggestions(suggestions);
}

This is almost identical, especially the way the example was written. I understand the differences, I don't think they're a problem for by far most cases where you'd want this.

A function is strictly more complex than a block because you have to define it first, then to invoke it. As a consequence, it is more code to write and read. Unless you have a good reason for it (which is not suggested by the original problem), why would you prefer the more complex way?

# Augusto Moura (6 months ago)

I see this operator quite confusing, in my opinion it's a best practice treat the functions (and errors) separately. If you want to ignore the second error you can even create a silent helper.

const treatedShowSuggestions = (suggs) => {
  try {
    showSuggestions(suggs);
  } catch (e) {
    // treat error
};

try {
  const suggestions = await fetchSuggestions();
  treatedShowSuggestions(suggestions);
} catch (e) {
  alert('Failed to load suggestions');
}

or

const silent = (fn) => {
  try {
    fn();
  } catch (e) {}
};

try {
  const suggestions = await fetchSuggestions();
  silent(() => showSuggestions(suggestions));
} catch (e) {
  alert('Failed to load suggestions');
}

This isn't even a workaround, it's just the right approach for what you want. If you wanna to evict the separated functions you can just inline the try/catch in the main function.

# Alan Plum (6 months ago)

The idea isn't to make the second call's exceptions silent, it's not to catch them (i.e. let them propagate).

# Waldemar Horwat (6 months ago)

On 02/08/2018 06:50, Alan Plum wrote:

I realise there is some ambiguity in using the else keyword for this (though I can't think of a meaningful opposite of "catch" either).

Indeed. You can't use 'else' without breaking existing behavior. For example:

if (foo) try {...} catch (e) {...} else {...}

 Waldemar
# Alan Plum (6 months ago)

Yikes, thanks for pointing that out. I guess this could be resolved by having a lower precedence for catch/else than if/else or by enforcing the sequence try/else/catch (as try without catch or finally is a syntax error).

# Isiah Meadows (6 months ago)

If you did else before catch/finally, that'd solve your problem. ;-)

The catch with finally (no pun intended) is this: does/should it execute before or after else?

Isiah Meadows me at isiahmeadows.com

Looking for web consulting? Or a new website? Send me an email and we can get started. www.isiahmeadows.com

# Alan Plum (6 months ago)

Going with python's semantics, finally should execute after else because else behaves analogous to catch. It's basically a "nocatch".

# T.J. Crowder (6 months ago)

On Tue, Feb 13, 2018 at 11:30 AM, Isiah Meadows <isiahmeadows at gmail.com>

wrote:

If you did else before catch/finally, that'd solve your problem. ;-)

The catch with finally (no pun intended) is this: does/should it execute before or after else?

Logically it makes sense finally would be after, so putting the else after try and before catch/finally makes sense for that reason as well:

try {
    console.log('a');
    a();
} else {
    console.log('b');
    b();
} catch (e) {
    console.log('c');
    c();
} finally {
    console.log('d');
    d();
}

Then

  • If a doesn't throw: a, b, d.
  • If a throws: a, c, d.
  • If b throws: a, b, c, d
  • If a and c both throw: a, c, d
  • If b and c both throw: a, b, c, d

else doesn't make sense in English terms anymore, though. then would though, and on first blush would be consistent with what the promise version of this would look like:

console.log("a");
a()
.then(() => {
    console.log("b");
    return b();
})
.catch(() => {
    console.log("c");
    return c();
})
.finally(() => {
    console.log("d");
})

-- T.J. Crowder

# T.J. Crowder (6 months ago)

On Tue, Feb 13, 2018 at 11:54 AM, T.J. Crowder <tj.crowder at farsightsoftware.com> wrote:

Then

  • If a doesn't throw: a, b, d.
  • If a throws: a, c, d.
  • If b throws: a, b, c, d
  • If a and c both throw: a, c, d
  • If b and c both throw: a, b, c, d

Gah, I forgot that you don't want the else block subject to the catch. So ignore that, and it no longer resembles the promise version.

Don't like else in front of catch if exceptions from the code in the else isn't caught.

-- T.J. Crowder

# Alan Plum (6 months ago)

Not quite. If the else (or nocatch, whatever) block throws, it would bypass the catch block but still hit the finally block the same way an exception in the catch block would:

  • if a doesn't throw: a, b, d
  • if a throws: a, c, d
  • if b throws: a, b, d
  • if a and c throw: a, c, d

This is analogous to this promise equivalent:

console.log("a");
a()
.then(() => {
    console.log("b");
    return b();
}, () => {
    console.log("c");
    return c();
})
.finally(() => {
    console.log("d");
})

(note the use of both arguments to then)

Your reading wouldn't really provide anything that can't be done by simply moving the else block's content into the try block.

# T.J. Crowder (6 months ago)

On Tue, Feb 13, 2018 at 12:37 PM, Alan Plum <me at pluma.io> wrote:

Not quite.

You appear to have missed my message three minutes later, after I'd realized that: esdiscuss.org/topic/try-catch-else#content-16

-- T.J. Crowder

# Alan Plum (6 months ago)

Oops, you're right. I agree that else might not be the best keyword for this all things considering.