RefactorMCP catalog

Replace Error Code with Exception

Makes a method that reports failure through its return value return nothing and throw instead, and turns each caller that tested the value into a try/catch.

This is a generator: it changes how failure travels, so the fixtures pin one chosen design rather than the only correct answer.

Arguments

ArgumentRequiredMeaning
exceptionnoThe exception type to throw and catch, by simple or qualified name. Defaults to InvalidOperationException

The target is the method, by symbol: "target": { "symbol": "M:Shop.Account.Withdraw(System.Decimal)" }.

Precondition

  • The method returns int, where 0 means success and any other value is an error code, or bool, where true means success.
  • Every return returns a constant, and at least one returns a failure.
  • The method is not virtual, abstract, an override or an interface implementation, since the other implementations would keep returning codes.
  • The exception type exists, is accessible and derives from Exception.
  • Every caller either ignores the result or tests it for success as the whole condition of an if: M() != 0, M() == 0, 0 != M() for an int; M() or !M() for a bool.
  • The result compiles.

Transformation

  • The return type becomes void.
  • A success return becomes return;, which is dropped when it is the last statement of the body and carries no comment.
  • A failure return becomes throw new InvalidOperationException(...). When the exception type has a constructor taking a string, the message is "<Method> returned error code <code>" for an int and "<Method> failed" for a bool; otherwise the parameterless constructor is used.
  • An expression-bodied method that always fails gets a block body that throws.
  • A caller's if becomes:

csharp try { <the call>; <the branch taken on success> } catch (InvalidOperationException) { <the branch taken on failure> }

A missing branch leaves its block empty.

  • using System; is added where the default exception is named.

Behaviour changes

  • A caller that ignored the result now sees the exception instead of carrying on. Such calls are left as they are.
  • Error codes are no longer distinguishable by value, except through the exception's message.
  • Statements of the success branch run inside the try, so an exception of the same type thrown there is caught by the failure branch.

Preserved

  • Which branch runs for each outcome of the call, for every caller the tool rewrites.
  • Comments on the rewritten returns and on the statements of both branches.
  • Other overloads of the method and their callers.

Limitations

  • Only 0 (or true) means success. A method for which some other value also means success, such as a positive count, has those returns turned into throws.
  • Callers that store the result, return it, combine it with other conditions or pass the method as a delegate are refused.
  • Each error code is not given its own exception type.

Error codes

CodeMeaning
in-hierarchythe method is virtual, abstract, an override or an interface implementation
unsupported-return-typethe method returns neither int nor bool
non-constant-returna return returns a value that is not a constant
no-error-codethe method never returns a failure
exception-type-not-foundno accessible exception type has the given name
unsupported-callera caller uses the result other than by testing it in an if
breaks-compilationthe result would not compile

Cases

·

Diffs show each file from before to after. /*[*/ … /*]*/ marks a selection and /*^*/ a caret; the runner removes them before the refactoring runs.

#bool-result

Treats true as success for a bool result; a check with an else puts the success branch in the try and the failure branch in the catch

success
Targetsymbol M:Shop.Store.Add(System.String)
Store.csmodified
1+using System;
12 using System.Collections.Generic;
23
34 namespace Shop
45 {
56 public class Store
67 {
78 private readonly List<string> _items = new List<string>();
89
9− public bool Add(string item)
10+ public void Add(string item)
1011 {
1112 if (string.IsNullOrEmpty(item))
1213 {
13− return false;
14+ throw new InvalidOperationException("Add failed");
1415 }
1516
1617 if (_items.Contains(item))
1718 {
18− return false;
19+ throw new InvalidOperationException("Add failed");
1920 }
2021
2122 _items.Add(item);
22− return true;
2323 }
2424
2525 public string Describe(string item)
2626 {
27− if (!Add(item))
27+ try
28+ {
29+ Add(item);
30+ return "Added " + item;
31+ }
32+ catch (InvalidOperationException)
2833 {
2934 return "Rejected " + item;
30− }
31− else
32− {
33− return "Added " + item;
3435 }
3536 }
3637
3738 public int Count(string item)
3839 {
39− if (Add(item))
40+ try
41+ {
42+ Add(item);
4043 return 1;
44+ }
45+ catch (InvalidOperationException)
46+ {
47+ }
4148 return 0;
4249 }
4350 }
4451 }

#caller-would-not-compile

Refuses when the result would not compile, here because the failure branch of an iterator yields, which a catch cannot

refusal
Targetsymbol M:Shop.Account.Withdraw(System.Decimal)
Refusesbreaks-compilation mentioning “Statement.cs”; every file is left unchanged
Account.csinput
1 namespace Shop
2 {
3 public class Account
4 {
5 private decimal _balance;
6
7 public int Withdraw(decimal amount)
8 {
9 if (amount > _balance)
10 return 1;
11
12 _balance -= amount;
13 return 0;
14 }
15 }
16 }
Statement.csinput
1 using System.Collections.Generic;
2
3 namespace Shop
4 {
5 public class Statement
6 {
7 public IEnumerable<string> Lines(Account account)
8 {
9 if (account.Withdraw(5m) != 0)
10 {
11 yield return "Declined";
12 }
13
14 yield return "Done";
15 }
16 }
17 }

#comments-and-blank-lines

Keeps comments on the returns it replaces and on the caller's check, and the final return when a comment sits on it

success
Targetsymbol M:Shop.Door.Close
Door.csmodified
1+using System;
2+
13 namespace Shop
24 {
35 public class Door
46 {
57 private bool _closed;
68 private int _warnings;
79
8− public int Close()
10+ public void Close()
911 {
1012 // Closing twice is a mistake.
1113 if (_closed)
12− return 1; // already closed
14+ throw new InvalidOperationException("Close returned error code 1"); // already closed
1315
1416 _closed = true;
1517
1618 // Closed now.
17− return 0;
19+ return;
1820 }
1921
2022 public void Leave()
2123 {
2224 // Close before leaving.
23− if (Close() != 0)
25+ try
26+ {
27+ Close();
28+ }
29+ catch (InvalidOperationException)
2430 {
2531 // Someone closed it already.
2632 _warnings++;
2733 }
2834
2935 _closed = false;
3036 }
3137 }
3238 }

#custom-exception

Throws and catches the named exception type, constructed without a message when it has no string constructor

success
Targetsymbol M:Shop.Hall.Reserve(System.Int32)
Arguments
exception"SeatsUnavailableException"
Hall.csmodified
11 namespace Shop
22 {
33 public class Hall
44 {
55 private int _free = 10;
66
7− public int Reserve(int seats)
7+ public void Reserve(int seats)
88 {
99 if (seats > _free)
10− return -1;
10+ throw new SeatsUnavailableException();
1111
1212 _free -= seats;
13− return 0;
1413 }
1514
1615 public string Book(int seats)
1716 {
1817 string answer;
19− if (Reserve(seats) == 0)
18+ try
2019 {
20+ Reserve(seats);
2121 answer = "Booked";
2222 }
23− else
23+ catch (SeatsUnavailableException)
2424 {
2525 answer = "Full";
2626 }
2727
2828 return answer;
2929 }
3030 }
3131 }
SeatsUnavailableException.csunchanged
11 using System;
22
33 namespace Shop
44 {
55 public class SeatsUnavailableException : Exception
66 {
77 }
88 }

#exception-type-not-found

Refuses an exception type that does not exist or does not derive from Exception

refusal
Targetsymbol M:Shop.Account.Withdraw(System.Decimal)
Arguments
exception"Account"
Refusesexception-type-not-found mentioning “Account”; every file is left unchanged
Account.csinput
1 namespace Shop
2 {
3 public class Account
4 {
5 private decimal _balance;
6
7 public int Withdraw(decimal amount)
8 {
9 if (amount > _balance)
10 return 1;
11
12 _balance -= amount;
13 return 0;
14 }
15 }
16 }

#in-hierarchy

Refuses a virtual method, whose overrides would keep returning codes

refusal
Targetsymbol M:Shop.Job.Run
Refusesin-hierarchy; every file is left unchanged
Job.csinput
1 namespace Shop
2 {
3 public class Job
4 {
5 public virtual int Run()
6 {
7 return 1;
8 }
9 }
10
11 public class CleanupJob : Job
12 {
13 public override int Run()
14 {
15 return 0;
16 }
17 }
18 }

#int-codes-caller-in-another-file

Makes a method returning 0 for success void, throws for each other code, and turns a caller's check of the code into a catch

success
Targetsymbol M:Shop.Account.Withdraw(System.Decimal)
Account.csmodified
1+using System;
2+
13 namespace Shop
24 {
35 public class Account
46 {
57 private decimal _balance;
68
7− public int Withdraw(decimal amount)
9+ public void Withdraw(decimal amount)
810 {
911 if (amount <= 0)
10− return 1;
12+ throw new InvalidOperationException("Withdraw returned error code 1");
1113 if (amount > _balance)
12− return 2;
14+ throw new InvalidOperationException("Withdraw returned error code 2");
1315
1416 _balance -= amount;
15− return 0;
1617 }
1718 }
1819 }
Teller.csmodified
1+using System;
2+
13 namespace Shop
24 {
35 public class Teller
46 {
57 public string Pay(Account account, decimal amount)
68 {
7− if (account.Withdraw(amount) != 0)
9+ try
10+ {
11+ account.Withdraw(amount);
12+ }
13+ catch (InvalidOperationException)
814 {
915 return "Declined";
1016 }
1117
1218 return "Paid";
1319 }
1420
1521 public void Drain(Account account)
1622 {
1723 account.Withdraw(100m);
1824 }
1925 }
2026 }

#no-error-code

Refuses a method that only ever returns success

refusal
Targetsymbol M:Shop.Account.Deposit(System.Decimal)
Refusesno-error-code; every file is left unchanged
Account.csinput
1 namespace Shop
2 {
3 public class Account
4 {
5 private decimal _balance;
6
7 public int Deposit(decimal amount)
8 {
9 _balance += amount;
10 return 0;
11 }
12 }
13 }

#non-constant-return

Refuses a method that returns a computed code, since it cannot tell success from failure

refusal
Targetsymbol M:Shop.Account.Withdraw(System.Decimal)
Refusesnon-constant-return mentioning “_lastError”; every file is left unchanged
Account.csinput
1 namespace Shop
2 {
3 public class Account
4 {
5 private decimal _balance;
6 private int _lastError = 3;
7
8 public int Withdraw(decimal amount)
9 {
10 if (amount > _balance)
11 return _lastError;
12
13 _balance -= amount;
14 return 0;
15 }
16 }
17 }

#nullable-annotations

Keeps nullable annotations on the parameters and handles a negated bool check with nullable analysis enabled

success
Targetsymbol M:Shop.Registry.Register(System.String)
Projectnullable enable
Registry.csmodified
1+using System;
12 using System.Collections.Generic;
23
34 namespace Shop
45 {
56 public class Registry
67 {
78 private readonly HashSet<string> _names = new HashSet<string>();
89
9− public bool Register(string? name)
10+ public void Register(string? name)
1011 {
1112 if (name is null || !_names.Add(name))
12− return false;
13−
14− return true;
13+ throw new InvalidOperationException("Register failed");
1514 }
1615
1716 public string? Welcome(string? name)
1817 {
19− if (!Register(name))
18+ try
19+ {
20+ Register(name);
21+ }
22+ catch (InvalidOperationException)
23+ {
2024 return null;
25+ }
2126
2227 return "Welcome " + name;
2328 }
2429 }
2530 }

#overloads

Changes only the targeted overload and the calls that bind to it

success
Targetsymbol M:Shop.Mailer.Send(System.String)
Mailer.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public class Mailer
66 {
7− public int Send(string message)
7+ public void Send(string message)
88 {
99 if (message.Length == 0)
10− return 1;
11− return 0;
10+ throw new InvalidOperationException("Send returned error code 1");
1211 }
1312
1413 public int Send(string message, int retries)
1514 {
1615 if (retries < 0)
1716 return 2;
1817 return 0;
1918 }
2019
2120 public void Notify()
2221 {
23− if (Send("hi") != 0)
22+ try
23+ {
24+ Send("hi");
25+ }
26+ catch (InvalidOperationException)
27+ {
2428 Console.WriteLine("failed");
29+ }
2530 if (Send("hi", 3) != 0)
2631 Console.WriteLine("failed again");
2732 }
2833 }
2934 }

#unsupported-caller

Refuses when a caller uses the code other than by testing it for success in an if

refusal
Targetsymbol M:Shop.Account.Withdraw(System.Decimal)
Refusesunsupported-caller mentioning “Audit.cs(7”; every file is left unchanged
Account.csinput
1 namespace Shop
2 {
3 public class Account
4 {
5 private decimal _balance;
6
7 public int Withdraw(decimal amount)
8 {
9 if (amount > _balance)
10 return 1;
11
12 _balance -= amount;
13 return 0;
14 }
15 }
16 }
Audit.csinput
1 namespace Shop
2 {
3 public class Audit
4 {
5 public int Record(Account account)
6 {
7 var code = account.Withdraw(5m);
8 return code;
9 }
10 }
11 }

#unsupported-return-type

Refuses a method returning something other than an int or bool code

refusal
Targetsymbol M:Shop.Account.Withdraw(System.Decimal)
Refusesunsupported-return-type; every file is left unchanged
Account.csinput
1 namespace Shop
2 {
3 public class Account
4 {
5 private decimal _balance;
6
7 public string Withdraw(decimal amount)
8 {
9 if (amount > _balance)
10 return "overdrawn";
11
12 _balance -= amount;
13 return null;
14 }
15 }
16 }