18
let x;
try {
  x = ...;
} catch (e) { 
  return
}

// rest of the code that uses `x`
const y = x + ...;

x is assigned exactly once, yet I have to use let instead of const.

The other way would be:

try {
  const x = ...;
  // rest of the code that uses `x`
  const y = x + ...;
} catch (e) { 
  return
}

However, this increases nesting and makes it unclear what can throw an error.

Is there a better way?

I don't have to care about the value of x if the try fails, as I'll be returning in the catch block.
I also don't want to extract it out to separate functions.

Avery235
  • 4,756
  • 12
  • 49
  • 83
  • These are two different ways. This depends on how `y` is used. Your code doesn't show that. – Estus Flask May 12 '18 at 09:45
  • What elaboration on the usage are you looking for? `y` could be used in several subsequent statements, eg `const z = y + ...`. – Avery235 May 12 '18 at 09:48
  • If `x` was created from an `await`ed promise, [you could use `.then()` for this](https://stackoverflow.com/q/44663864/1048572) – Bergi May 12 '18 at 09:49
  • This is about how errors in the code that uses `y` should be handled. In second example the error in y code will be suppressed. In first example it will be thrown, so it could be handled. This depends on what you want. – Estus Flask May 12 '18 at 09:50
  • @estus If the rest of the code could throw an error, I'll wrap them in a separate `try` so that I can handle them differently in the `catch`. – Avery235 May 12 '18 at 09:54
  • 1
    `const` is less useful in imperative style, because rebindings are common. So don't fight the style you program in, but just use `let`. Alternatively, you can switch to the functional style. Then `const` will be your friend. –  May 12 '18 at 12:29
  • @ftor how can I switch to "functional style" in this case? – Avery235 May 12 '18 at 12:38
  • You can't just switch. Using an error type in FP requires experience and prior knowledge. Besides, Javascript doesn't help you much, because most of its ecosystem is geared towards imperative programming. –  May 12 '18 at 12:47
  • "Using an error type in FP requires experience and prior knowledge" what does that mean? "experience and prior knowledge"? Your advise to "switch to the functional style" is followed by "you can't just switch"? – Avery235 May 12 '18 at 12:53
  • Sorry, my FP advice was a bit sloppy and meant in the long term. You can't just switch the paradigm, because you need to get familiar with the new one. This requires time. The learning curve is quite steep. My short term advice is: Stick to `let`. –  May 12 '18 at 13:31
  • Maybe you can explain how the above code can be converted into FP style? I don't think my _presumed_ lack of experience with FP is relevant to how it can be applied in this context. – Avery235 May 12 '18 at 13:36
  • Use an option (aka maybe) type. This is a tagged union which represents either a value produced by a computation, or nothing (a computation without a result). The latter case short circuits the computation. –  May 12 '18 at 13:56
  • 1
    Can you post an answer with some brief javascript code snippets? – Avery235 May 12 '18 at 13:58
  • Some languages, like Scala, allow the value of the 'try' block to be passed to the outside. This eliminates the issue, without needing to do FP humdrums. Unfortunately, EcmaScript 'try' seems to be non-value-passing. – akauppi May 21 '20 at 21:41

5 Answers5

1

Whenever I run across something like this, I use a function:

function constTryCatch(valueFn, catchFn) {
  try {
    return valueFn();
  } catch (e) {
    if (catchFn) catchFn(e);
    return null;
  }
}

const obj = { foo: 'bar' };
const x = constTryCatch(() => obj.foo);
console.log(x);
const y = constTryCatch(() => obj.foo.bar.baz, (e) => console.log(e));
console.log(y);
// example, if the rest of the block depends on `y` being truthy:
// if (!y) return;

Note, the stack snippet doesn't display the error properly. In the real browser console, you'll see something like this:

bar

TypeError: Cannot read property 'baz' of undefined at constTryCatch ((index):79) at constTryCatch ((index):69) at window.onload ((index):79)

null

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • 2
    This doesn't abort (`return` from the whole code) though in the `catch` case, it does assign `null` to `x` and continues. – Bergi May 12 '18 at 09:52
  • That's what the `if (!y) return;` line is for. – CertainPerformance May 12 '18 at 10:17
  • 3
    Ah, hadn't noticed that. However, it checks only whether `y` is truthy, not whether no error had been thrown. If I want the code to work with arbitrary values for `.baz` (including falsy ones), I need an extra flag. – Bergi May 12 '18 at 13:31
1

Go functional - use a helper function with three callbacks:

function Try(attempt, onSuccess, onFailure) {
  try {
    var res = attempt();
  } catch(err) {
    return onFailure(err);
  }
  return onSuccess(res);
}

This allows you to write

return Try(() => …, x => {
  // rest of the code that uses `x`
  const y = x + …;
}, e => void e);

You can also make use of a data structure representing this control flow, like the Result monad (also known as the Either monad):

class Result {
  constructor(go) {
    this.go = go;
  }
  static Ok(v) {
    return new this((onSuccess, _) => onSuccess(v));
  }
  static Err(r) {
    return new this((_, onFailure) => onFailure(v));
  }
  map(f) {
    return this.go(v => Result.Ok(f(v)), r => Result.Err(r));
  }
  chain(f) {
    return this.go(v => f(v), r => Result.Err(r));
  }
  unwrap() {
    return this.go(v => v, r => { throw r; });
  }
}
function Try(attempt) {
  try {
    var res = attempt();
    return Result.Ok(res);
  } catch(e) {
    return Result.Err(e);
  }
}

You could use it very similar to the above simple helper function:

return Try(() =>
  … // exceptions in here are caught
).go(x => {
  // rest of the code that uses `x` - exceptions are not caught
  const y = x + …;
}, e => void e);

But also with more advanced chaining:

return Try(() =>
  … // exceptions in here are caught
).chain(x =>
  Try(() =>
    x + … // exceptions in here are caught as well
  )
).map(y =>
  … // exceptions in here are not caught
).unwrap(); // any caught exceptions are re-thrown
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
0

I would extract simple function to do the logic and error handling:

function getXOrHandleError() {
  try {
    return ...
  } catch (e) {
    // handle error
    return
  }
}

const x = getXOrHandleError()
// ...
madox2
  • 49,493
  • 17
  • 99
  • 99
0

Using some functional standard library like SanctuaryJS you might rewrite your code using Either monad as follows:

// encaseEither wraps a function so it handles a throw and
// gets the Error object and builds a Left value.
const parseJson = S.encaseEither ( S.I ) ( JSON.parse )

// This will end up in Right, because given JSON
// could be parsed.
const eitherJson = parseJson ( '[ 1, 2, 3 ]' )

// However, this will end up in Left, because given input
// isn't a valid JSON
const eitherJson2 = parseJson ( '{' )

// Sets [ 1, 2, 3 ]
const result = S.fromEither ( [] ) ( eitherJson )

// Sets an empty array
const result2 = S.fromEither ( [] ) ( eitherJson2 )

BTW, if you're calling your own functions, you should avoid throw as much as possible and use Maybe or Either:

const someFun = x => x < 5 ? S.Nothing : S.Just ( x * 2 )

// Sets Just ( 20 )
const maybeResult = someFun ( 10 )

// Sets  Nothing
const maybeResult2 = someFun ( 3 ) 

const sumFun2 = x => x * 3

// Just ( 30 )
const maybeResult3 = S.map ( somFun2 ) ( maybeResult )

// Nothing
const maybeResult4 = S.map ( someFun2 ) ( maybeResult2 )

// Get maybeResult3 or output a default
// So it sets 30
const result3 = S.fromMaybe ( 0 ) ( maybeResult3 )

// Get maybeResult3 or output a default
// So it sets 0
const result4 = S.fromMaybe ( 0 ) ( maybeResult4 )

I would completely avoid exceptions. And for those functions from libraries and frameworks, I would encase them all to express effects using monads.

Matías Fidemraizer
  • 63,804
  • 18
  • 124
  • 206
-1

The way this should be done depends on how errors should be handled. If they should be handled differently and they can be conditionally handled, there can be a single try...catch {

try {
  const x = ...;
  // rest of the code that uses `x`
  const y = x + ...;
  ...
} catch (e) { 
  if (e ...)
    return;
  else if (e ...)
    ...
  else
    ...
}

Alternatively, errors can be handled in caller function.

If errors cannot be conditionally handled, then there should be several try...catch:

let x;
try {
  x = ...;
} catch (e) { 
  return;
}

// rest of the code that uses `x`
try {
  const y = x + ...;
  ...
} catch (e) { 
  ...
}

Since x is block scope variable and needs to be used in another scope, it needs to be declared with let in parent scope.

Another way is to nest try...catch, this allows to prevent multiple assignments of x but increases nesting level:

try {
  const x = ...;

  // rest of the code that uses `x`
  try {
   const y = x + ...;
    ...
  } catch (e) { 
    ...
  }
} catch (e) { 
  return;
}
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • It can't be conditionally handled (the `catch` handles it based on which function throws the error rather than what error is thrown). I'm using the second approach, just didn't find it clean as `let` allows it to be reassigned later on even though the intend is to reassign it exactly once 2 lines below it. – Avery235 May 12 '18 at 10:06
  • Yes, reassignments is a problem with let. An alternative is `var` which is generally unfavourable because it allows to assign it on same line but allows reassignments and leaks the variable to enclosing scope. I updated the answer with another alternative. I generally won't go with nested try..catch unless I have real problems with unwanted reassignments or uncaught errors. – Estus Flask May 12 '18 at 10:18